├── README.md ├── TCD1304Rev2KiCAD ├── TCD1304Rev2.kicad_pcb ├── TCD1304Rev2.kicad_prl ├── TCD1304Rev2.kicad_pro └── TCD1304Rev2.kicad_sch ├── TCD1304Rev2_Python ├── Accumulators.py ├── GUIWindow.py ├── GraphicsWindow.py ├── TCD1304Rev2Controller.py ├── help.txt └── tcd1304rev2controller.history ├── TCD1304Rev2b.asc ├── TCD1304Rev2b_Firmware.230301 └── TCD1304Rev2b_Firmware.230301.ino └── TCD1304Rev2c_KiCAD ├── TCD13042c.kicad_pcb ├── TCD13042c.kicad_pro ├── TCD13042c.kicad_sch ├── TCD13042c.pdf └── TCD13042c_BOM.xls /README.md: -------------------------------------------------------------------------------- 1 | 2 | # Introduction 3 | Linear CCDs can be useful tools in science, for example as a sensor for a spectrometer or imaging system. And indeed, there are many commercial instruments that are based on low cost linear CCDs such as the Toshiba TC1304 and the Sony IXL511. 4 | Here we provide design files, firmware and software for a device that provides competitive performance and a rich set of science-centric features, and that can be customized to your experiments, all at a fraction of the cost of the commercial offerings. The design is based on the Toshiba TCD1304 (3648/3694 pixels) and Teensy 4.0 (600MHz ARM, 480MHz USB). The Teensy 3.2 is plug compatible for this design. 5 | Read further below, and you will find tutorials on electrical design and managing the CCD to provide flexible timing and reproducible measurements so that you can use it as a scientific instrument. 6 | 7 | In this repository you will find directories containing (a) electrical design files in KiCad, (b) firmware (a sketch file) for the Teensy, and (c) a Python class library with graphical and command line utilities to operate the device. 8 | The device has a trigger or gate input, sync and busy outputs, and spare pins for digital and analog I/O. 9 | The operating modes include clocked, triggered frames, triggered series of frames, and gated frames (where the shutter is opened and closed on the rising and falling edges of the logic level input signal). 10 | The device appears as a serial port (COM in windows) with comands and response in human language ASCII and frames returned as binary or formatted. 11 | The python class library provides higher level functions, a graphical realtime display and a command line processor. There is a help command in the firmware and in the Python command line interpreter. Details of the electrical and firmware design are described later in this README. 12 | 13 | Since the original upload, we have added new sections on driving the shift and integration clear gates, the analog interface to the microcontroller, and precision (number of bits) versus integration time. 14 | 15 | The following shows the microcontroller side of the device. The sensor side can be seen in the image of the spectrometer (below). The digital I/O pins across the top include trigger input, sync and busy output, pins that monitor the signals going to the sensor, and spares that can be controlled through the user interface. Across the bottom there are pins that can be used for analog inputs or digital I/O, and 3.3V that can be used for an auxiliary device. 16 | 17 | ![IMG_20231215_144019112_cropped250](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/1eda6d73-2e27-4ffd-ba63-d32f814700c4) 18 | 19 | 20 | Following are two examples of applications; using the sensor in a spectrometer and measurement of spectral-spatial-dynamics in an OLED. This repository provides what you need in terms of ecad files and software to build one for yourself, or if you like you can contact me for an assembled board. 21 | 22 | The files in the repo were produced on a Linux desktop computer (Fedora 37, Cinnamon Spin), using KiCAD 6 for the electrical design and Emacs for the firmware and python programs. 23 | 24 | ## Some Example Use Cases 25 | 26 | ### Spectrometer 27 | The first shows the inside of a spectrometer built with a G1200 grating, two lenses and a slit. The mounts are 3D printed and everything is affixed to a stiff Al plate. There is a cover, not shown here, and specific surfaces are covered with low-reflectance tape. The spectrum is that of a fluorescent ceiling light through a 200um fiber at a distance of about 8ft, with a 200um slit. 28 | 29 | |![IMG_20231217_120419719 cropped350](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/df8263ad-cb4c-41de-bd71-2716a8124167)|![FluorescentLamp Screen500](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/6d012101-5bae-4bd2-b496-b92bc3603f1b)| 30 | 31 | ### Imaging Spectral-Spatial-Dynamics 32 | The following example uses the sensor device to record the time evolution of the spatial distribution of light produced by an OLED at a current density of 150 $\mu A/cm^2$. The sensor device collects a series of frames wth 10ms shutters at 10ms intervals (back to back), with the series initated on a trigger at the start of the applied voltage waveform. The blue line shows the resulting current density in the OLED, the pale line shows the sync pulses output by the sensor device at the start of each frame, and the red line is the applied voltage (2.41V). (We plan to post repositories for the DAQ board and current amplifier as well.) 33 | 34 | |![SampleR1 51 D7_G100K_A2 410v_I0 010s_D0 500s_N005_T4 0C_M17 0000mm_W501 340nm 20230309 080608 827395 tdaq2](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/93a6eb79-6a26-4ebe-8d46-3b25e46819c1)|![SampleR1 51 D7_G100K_A2 410v_I0 010s_D0 500s_N005_T4 0C surface cropped](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/c7ad51c5-3052-4c0e-905a-4b4ebf87df7e)| 35 | 36 | # Electrical Design, Analog 37 | The datasheet for the TCD1304 can be found here https://toshiba.semicon-storage.com/us/semiconductor/product/linear-image-sensors/detail.TCD1304DG.html. 38 | The following table is found on page 4. 39 | We see that $V_{SAT}$ the saturation output volage runs from 450mV to 600mV, $V_{MDK}$ the dark signal is 2mV, thus a dynamic range of 300 (for a 10ms integration integral), $V_{OS}$ the DC output signal is typically 2.5V but can be from 1.5V to 3.5V, and $Z_O$ the output impedance is typically 500 ohms but can be 1K. So, that is a lot of variation that we need to account for in our design. 40 | 41 | ![TCD1304-optical-electrical](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/7c0a2a66-d456-45e5-9f44-d4f6856260c5) 42 | 43 | The datasheet provides the following diagram in Note 8 to illustrate the definition of $V_{OS}$. The hatched area is the negative going output signal. SS is ground 44 | 45 | ![TCD1304-electrical-note8](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/ceaa5288-26fd-4485-ae8d-bf512b9fe894) 46 | 47 | For best performance in digitizing the signal, we want a circuit that takes the above signal as input and outputs a signal that matches the input range of our microcontroller's ADC. The following circuit element shows one approach that does this and can run on a single supply. We can call this a shift, flip and amplify (SFA). The gain is set as usual for an inverting amplifier by $R_2$ and $R_1$ and the potentiometer provides the offset through the + input of the opamp. 48 | 49 | ![SimpleSchematic1](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/0bc8349a-38d2-42b1-843e-d70b2531a980) 50 | 51 | But, recall that the datasheet says the output impedance $Z_0$ varies from 500 ohms to 1k. If we connect that directly to our SFA then $Z_0$ becomes part of the gain equation, $R1\rightarrow R1+Z_0$, and in a bag of sensors we might find that the gain is different for each sensor. 52 | So, we need to isolate that source impedance from the SFA. 53 | 54 | A solution to this is to us an opamp follower as the first stage, as shown in the following crcuit. The follower presents a very high impedance at its input, which is ideal for reading the voltage from the sensor, and a low impedance at its output, which is ideal as an input to the SFA 55 | 56 | ![TCD1304-opampfollower](https://github.com/user-attachments/assets/01444fcd-368b-4a48-a75c-3357dc1dcdea) 57 | 58 | Referring again to the datasheet for the TCD1304, we see that (a) we can operate the sensor chip in the range 3V to 5.5V, (b) it requires a clock between 800KHz and 4MHz (2MHz if operated below 4V) and (c) the data readout rate is 1/4 of the clock 200kS/s to 1MS/s (500KS/S if powered below 4V). 59 | Operating with a 3.3V supply, and making efficient use of the full scale range of the ADC, means that we need to use a rail to rail opamp. 60 | Our SFA architecture means that the opamp has to also have a wide common mode range. 61 | And the sampling rate means we need to look for a slew at least as faster as 100V/usec 62 | 63 | The complete analog front end circuit is shown in the following LTSpice model based on the ADA4807. 64 | We use the first opamp in the package for the voltage follower and the second opamp for the flip, shift,and amplify stage. 65 | Gain and offset are as calculated above. 66 | The green trace is the output from the sensor and the purple curve is the output from the second stage. 67 | As can be seen the 2.5V to 1.9V signal from the sensor becomes a 0.1V to 3.1V for the ADC, and the rise time is small compared to the sampling period. 68 | In our actual circuit we use a trim pot for the voltage applied to V+. 69 | 70 | ![Screenshot from 2023-12-18 08-43-01](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/b380a297-6e34-4aad-a0bb-7b30448e554a) 71 | 72 | It might be noted that we could have chosen a larger gain to look at lower intensity light, or with the Teensy 3.2 we can use its built-in amplifier under software control, provided the offset is small. 73 | 74 | ## Analog input of the Teensy 4.x and Teensy 3.2 75 | The following diagaram for the analog input, and one similar to this, are found in the datasheets for the i.MXRT106x (Teensy 4) and the K20 (Teensy 3). 76 | This is a modified version of a SAR type analog input. Normally the input sees the switched sampling capacitor. 77 | Here there is a series resistor in front of the sampling capacitor. 78 | 79 | ![T3analoginput](https://github.com/user-attachments/assets/10ae87ce-2e72-4959-8f26-67fdcef17f1d) 80 | 81 | In operation, the switch closes for a period of time to allow the capacitor to draw charge from the input. 82 | At the end of the sampling period the switch is opened and the voltage on the capacitor is converted to a digital representation in the successive approximation register (SAR). 83 | In other words, precision is determined by the length of the sampling window (in time) versus the RC time constant seen by the sampling capacitor. 84 | Full precision means the voltage on the capacitor is within 1 LSB of the input, at the end of the sampling window. 85 | 86 | For a simple RC network, the voltage on the capacitor is $V(t)/V(0) = 1 - exp(-t/RC)$. 87 | Therefore, for n bits of precision, the sampling window needs to be at least $ln(2^{n}) \times RC$. 88 | This works out to be 11 x RC for 16 bits and 8 x RC for 12 bits. 89 | The following shows this graphically. 90 | 91 | ![samplingtime](https://github.com/user-attachments/assets/6a794891-e235-473a-8f6f-eafbd615cedd) 92 | 93 | For the T3 in 16 bit mode, with RADIN = 2k, and CADIN = 8pf, RC ~ 16nsecs. 94 | The voltage on the sampling capacitor is within $1/2^{16}$ of the input voltage after about 170nsecs. 95 | 96 | For the T4 in 12 bit mode, RADIN can be from 5k to 25k and CADIN is 1.5pF, RC ~ 7.5nsecs to 40nsecs. 97 | We need about 60nsecs to 320nsecs to reach 12 bit precision. 98 | 99 | Thus the T3 and T4 are both compatible at 500KSPS. See the [ADC library](https://github.com/pedvide/ADC) for details setting the sampling time and conversion clock. 100 | 101 | The tradeoff for the T3 with its 16 bit input, is that the T3 has a much slower USB interface, 1MB/s transfers to the host versus 60MB/s for the T4. 102 | The best frame to frame interval with the T3 is about 16msecs and about 8msecs for the T4. 103 | 104 | N.B. Driving the analog input of an MCU is different from driving a normal SAR type ADC. Normally, the drive circuit for a SAR includes an external capacitor to serve as a charge reservoir for the sampling capacitor. This is pre-empted in the MCU analog input by the large internal resistance in series with the sampling capacitor. 105 | 106 | ## When do I need 16 bits? 107 | In the table above, the dynamic range is listed as 300. 108 | This is simply the saturation output voltage 600mv divided by the dark signal 2mV. 109 | On face value, 10 good bits would be enough and the T4 is a good match. 110 | This is improved in two ways, by using shorter exposures since dark noise is proportional to exposure time, or by cooling the sensor. 111 | 112 | At a modestly shorter exposure time, 100usecs, the dark signal is 20uV, and the dynamic range becomes 30,000. 113 | For those short exxposures we need a 16 bit ADC, provided we are limited only by the dark noise. 114 | 115 | Our analog section uses an ADA4807. Its datasheet lists the input voltage noise as $3.1 nV/\sqrt Hz$. 116 | At 500KSPS this becomes 2.2uV, and setting the gain at 5, we expect 10uV. 117 | Without going into a more detailed analysis, we see that our electrical noise can be about 1/2 of the dark signal. 118 | That is workable with some signal averaging. 119 | 120 | Special lower noise sensor designs, with differential signal paths and ADC are posted at 121 | [TCD1304 with 16 bit differential ADC for SPI](https://github.com/drmcnelson/TCD1304-SPI) 122 | and 123 | [S11639-01 with 16 bit differentual ADC for SPI](https://github.com/drmcnelson/S11639-01-Linear-CCD-PCB-and-Code) 124 | We plan to upload a repo with a cooled sensor and a special high precision adc, in the near future. 125 | The Hamamatsu board has been built and tested. The first and third can be moved forward with sponsorship. 126 | 127 | # CCD operation 128 | Operationally, a CCD sensor stores charge in each pixel proportional to light and noise, until assertion of a shift pin causes the contents to be transferred to a buffer and then the contents are shifted along the buffer by a clock to the output pin and appear as a series of voltages. 129 | The TCD1304 has an additional function that controls which shift assertions initiate the readout sequence. 130 | The internal structure is depicted as follows from page 3 of its datasheet. Externally the device is controlled by three pins, shift SH, integration clear gate ICG, and master clock $\phi M$. 131 | 132 | ![TCD1304-registers](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/1865363d-bbbe-4902-be47-285b8f0ef6f8) 133 | 134 | The following two figures from the datasheet show how Toshiba envisions operation of the sensor chip. As indicated, charge is integrated during the intervals between trailing edges at the SH pin. When the the ICG pin is low the accumulated charges are available in the readout buffer and then shifted to the output pin at a rate of 1 datum per four cycles of the master clock. 135 | 136 | ![TCD1304-timing1](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/e0c361c6-3cdb-4eb6-9314-ea43b90607dd) 137 | 138 | ![TCD1304-timing2](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/18aa49f5-0524-4cec-be0e-52a2ee25068b) 139 | 140 | Toshiba labels the second diagram above as "Electronic Shutter Function". This refers to the function of ICG in selecting which SH assertion results in charge being available in the readout buffer. However it is not a shutter in the conventional sense. It is easily shown experimentally that if the device is left idle, several SH cycles are needed to arrive at a noise level baseline in the readout. There are a number of commerical CCD systems that clock the SH pin or its equivalent to keep the sensor "clean". This can have important ramifications if the device is to be triggered, for example for kinetic studies. Alternatives include good "dark" management, designing the experiment to start with a few blank frames to clear the sensor, and/or having the device initiate the trigger. 141 | 142 | Note that it is the low state on the ICG pin that makes the readout available to be clocked out to the OS pin, while the SH pin sets the integration interval. 143 | 144 | ## Driving the SH, ICG and Master Clock pins 145 | Referring again to the datasheet, page 6, we find the following table. 146 | Notice that the shift gate has a capacitance of 600pF and the integration clear gate has 250pF. 147 | From the magnitude of the capacitances, we might guess that these are closely related to moving charge to the output buffer. 148 | 149 | ![image](https://github.com/user-attachments/assets/b0cc4b91-a9f9-4a90-91d4-347e24e93084) 150 | 151 | Notice also, that the master clock and data transfer rates are reduced when operating at lower voltages. 152 | 153 | ![image](https://github.com/user-attachments/assets/4048145d-1f1a-4894-bab4-06ee4319979c) 154 | 155 | The large capacitances also factor into how we drive these pins, rise times with large capacitances are easily current limited. 156 | The preferred way to drive the gate pins is with a buffer and series resistor, as shown in the following. 157 | For a 3.3V drive, a 150 ohm resistor sets the rise to 90nsecs for the SH pin and limits the current to 22mA. 158 | For a 1 usec pulse, this gives a rise time that is less than 1/10 of the pulse width. 159 | The buffer can be a 74LVC1G34, or one channel of a 74VLC3G34, which can drive 25mA. 160 | 161 | ![singlegate](https://github.com/user-attachments/assets/c67f9783-9346-4ee8-9e86-164fbfa76bcd) 162 | 163 | Alternatively, all three channels of a 74LVC3G34 can be combined as in the following, to drive a total of 75mA. In this configuration, the rise time on the SH pin can be about 26nsecs. 164 | 165 | ![triplegate](https://github.com/user-attachments/assets/308c0ed6-acde-4bdb-b933-d6d29510eb8a) 166 | 167 | For comparison, the Teensy digital I/O pins provide 4mA. 168 | If the gate is driven directly from the Teensy, the response is current limited, $\Delta t \approx C \Delta V / I$. 169 | That works out to 500nsecs, or about 1/2 of the 1 usec pulse. 170 | 171 | In this repository, we have uploaded two board designs. 172 | The original, driving the SH, ICG and master clock directly, and another driving the gates and master clocks with buffers. 173 | 174 | Caveat, the buffer gate version is a simple modification from the direct-gate-drive board, but as of this writing I have not yet built one of these. 175 | (Click the sponsor button if you like, and send me a note. When sponsorships match costs, I'll build some and make then available). 176 | 177 | 178 | ## Frame rates, shutters and timing for data acquisition. 179 | The following diagram from page 9 of the datasheet shows the timing requirements for the ICG and SH pins relative to each other and the master clock. 180 | 181 | ![TCD1304-timingreqs](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/6256bbf6-0993-47ce-8623-0f77907d3063) 182 | 183 | With a 2MHz master clock, it takes about 7.4ms to read one record from the device into the memory of the microcontroller. Transfer from the microcontroller to the host PC can take an additional 5ms for the Teensy 3.x (12 Mb/s) or about 120usec with the Teensy 4.x (480 Mb/s). Needless to say, this sets the maximum frame rate, that is the time between readouts, and is different from the minimum integration interval which depends only on the minimal interval between successive trailing edges at the SH pin. 184 | 185 | For data collection, we need to be able to set integration and frame intervals freely (within the physical limits of the sensor) and we need to be able to reliably control timing with respect to an external trigger or gate. Since the shutter or integration interval is defined by successive assertions of the SH pin, we focus on how these requirements translate to operation of this pin. 186 | 187 | The following diagram shows a sequence where the shutter interval is shorter than the inter frame interval, and the frame interval is not necessarily an integer number of shutter intervals in length. The SH pin operates with alternating short and long intervals. The frame interval is the sum of these two intervals, or that betwen every second SH. 188 | 189 | ![Shutter-Operation-shortshutter](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/2f8c72b0-5ce8-4873-b6db-025a604f5a09) 190 | 191 | The following shows back to back shutters with identical frame and shutter intervals. 192 | 193 | ![Shutter-Operation-longshutter](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/ff287173-09d6-4ee7-a6b2-3ac014f4b3f3) 194 | 195 | And this one shows a constant shutter interval with the frame interval an integer multiple of the shutter. 196 | 197 | ![Shutter-Operation-modalshutter](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/040c281e-0778-4aa0-9e86-23f334caaec4) 198 | 199 | Our control logic for operating the device has to accomodate all three scenarios, and we have to be able to initiate frames or sets of frames from a clock or from a trigger. 200 | Architecturally, we set this up as two ISRs, which we call ShutterA_isr() and ShutterB_isr(). The sequences are easily implemented by various combinations of A and B or just B after one A, and easily triggered, gated or clocked as needed. 201 | 202 | ## Signals, Sync and Busy 203 | The sensor device has a trigger input and two outputs SYNC and BUSY. Sync can be configured to signal the start of a series of frames or to signal the start of each shutter. SYNC can also be configured to be followed by a holdoff. BUSY is set with the start of the first shutter and remains asserted until the last data transfer is complete. Each pin can be set nominally HIGH or LOW, assertion is the opposite. Additionally there is a SPARE pin. Any of the pins can be manually set, cleared, toggled or pulsed. 204 | 205 | The following shows the actual operation of the sensor device, green is SH, purple is ICG, blue is SYNC and yellow is BUSY. We see BUSY goes high immediately, SH and ICG operate as described in the data sheet. Light is integrated between the two trailing edges and the data is transferred following the second assertion of the SH and clearing of the ICG pins. 206 | 207 | ![Scope-singleframe](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/611557d1-e55c-48d0-b26d-84dc9b8b2dfe) 208 | 209 | ## Triggered and gated operation 210 | The following shows a triggered frame. The commands are "set trigger rising", "trigger 20" to trigger one 20usec frame. The blue line is the trigger input. There is a small reproducible delay between the trigger and the shutter, due to certain fixed timings involved in getting the shutter started. Notice the alignment of the BUSY and SH signals. 211 | 212 | ![Scope-triggerd](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/9310338d-311b-468d-9891-1daa79e466c9) 213 | 214 | The following shows a gated frame, blue is the gate signal. The commands are "set trigger change", "gate 1", to gate one frame. For this, the spare pin is connected to the trigger input and we enter the command "pulse spare 20" to output a 20 usec pulse. As you can see the shutter sequence begins and finishes with the gate. 215 | 216 | ![Scope-gated](https://github.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/assets/38619857/5682114e-f9e4-41eb-b6de-08bfdad62288) 217 | 218 | 219 | # Data processing 220 | Referring to the clock diagrams above, we see that the data record comprises 12 dummy outputs followed by 13 light shielded elements, followed by 3 shadowed elements, followed by the 3648 elements making up the effective output and followed by another 14 dummy elements. Thus elements 12 thru 24 provide a baseline which we can average or take the median and subtract from elements 28 through 2675 which form the image. In the spirit of "always preserve primary data", we do not do this substraction nor any scaling, in firmware. Rather we pass the entire record as is, to the PC host and the host software is responsible for subtracting and scaling as appropriate. 221 | 222 | # Firwmare 223 | The firmware subdirectory contains a sketch for Teensy 4.0. After assembling your board (or obtaining one from the author), you can mount your Teensy 4 and TCD1304 detector, and then flash the program into the Teensy 4.0. The commands are in english, ASCII over USB. Enter the command "help" to get a listing and short explanation of the commands. A listing of the help text is included in the Python subdirectory. 224 | 225 | The default action returns the data in binary. You will ideally want to use a multi-threaded program on your host computor, with one thead reading the responses and data and queuing as appropriate to a separate graphics thread. The Python code provides a Class for the device that handles all of this. 226 | 227 | # Python 228 | The Python subdirectory contains a file TCD1304Rev2Controller.py and three library files that it looks for in its directory. The program implements threads using the multitasking interface in Linux. A dedicated thread reads the responses from the device and queues data to an internal data queue for the "save" command and to a queue read by the runtime graphics thread. The Python program has a help command, like the firmware. 229 | 230 | # Adjusting the offset 231 | With a voltmeter or scope, the middle pin on the trim pot should be set close to 2.1 volts. Then in the Python program, issue the commands "baseline off" and "clock 1000 10000 100000". This wil turn off the baseline subtraction function and clock 1000 frames with an integration time of 10ms spaced at intervals of 100ms. Try the commands "stop", "baseline on" and repeat the clock command to see a comparison. You can zoom in on the graphical display and use a small screwdriver to adjust the offset. I usually put the sensor in a drawer or cover it with a black cloth while I do this. 232 | 233 | -------------------------------------------------------------------------------- /TCD1304Rev2KiCAD/TCD1304Rev2.kicad_prl: -------------------------------------------------------------------------------- 1 | { 2 | "board": { 3 | "active_layer": 0, 4 | "active_layer_preset": "", 5 | "auto_track_width": true, 6 | "hidden_netclasses": [], 7 | "hidden_nets": [], 8 | "high_contrast_mode": 0, 9 | "net_color_mode": 1, 10 | "opacity": { 11 | "images": 0.6, 12 | "pads": 1.0, 13 | "tracks": 1.0, 14 | "vias": 1.0, 15 | "zones": 0.6 16 | }, 17 | "selection_filter": { 18 | "dimensions": true, 19 | "footprints": true, 20 | "graphics": true, 21 | "keepouts": true, 22 | "lockedItems": false, 23 | "otherItems": true, 24 | "pads": true, 25 | "text": true, 26 | "tracks": true, 27 | "vias": true, 28 | "zones": true 29 | }, 30 | "visible_items": [ 31 | 0, 32 | 1, 33 | 2, 34 | 3, 35 | 4, 36 | 5, 37 | 8, 38 | 9, 39 | 10, 40 | 11, 41 | 12, 42 | 13, 43 | 15, 44 | 16, 45 | 17, 46 | 18, 47 | 19, 48 | 20, 49 | 21, 50 | 22, 51 | 23, 52 | 24, 53 | 25, 54 | 26, 55 | 27, 56 | 28, 57 | 29, 58 | 30, 59 | 32, 60 | 33, 61 | 34, 62 | 35, 63 | 36, 64 | 39, 65 | 40 66 | ], 67 | "visible_layers": "ffdffff_ffffffff", 68 | "zone_display_mode": 0 69 | }, 70 | "git": { 71 | "repo_password": "", 72 | "repo_type": "", 73 | "repo_username": "", 74 | "ssh_key": "" 75 | }, 76 | "meta": { 77 | "filename": "TCD1304Rev2.kicad_prl", 78 | "version": 3 79 | }, 80 | "project": { 81 | "files": [] 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /TCD1304Rev2KiCAD/TCD1304Rev2.kicad_pro: -------------------------------------------------------------------------------- 1 | { 2 | "board": { 3 | "3dviewports": [], 4 | "design_settings": { 5 | "defaults": { 6 | "apply_defaults_to_fp_fields": false, 7 | "apply_defaults_to_fp_shapes": false, 8 | "apply_defaults_to_fp_text": false, 9 | "board_outline_line_width": 0.1, 10 | "copper_line_width": 0.2, 11 | "copper_text_italic": false, 12 | "copper_text_size_h": 1.5, 13 | "copper_text_size_v": 1.5, 14 | "copper_text_thickness": 0.3, 15 | "copper_text_upright": false, 16 | "courtyard_line_width": 0.05, 17 | "dimension_precision": 4, 18 | "dimension_units": 3, 19 | "dimensions": { 20 | "arrow_length": 1270000, 21 | "extension_offset": 500000, 22 | "keep_text_aligned": true, 23 | "suppress_zeroes": false, 24 | "text_position": 0, 25 | "units_format": 1 26 | }, 27 | "fab_line_width": 0.1, 28 | "fab_text_italic": false, 29 | "fab_text_size_h": 1.0, 30 | "fab_text_size_v": 1.0, 31 | "fab_text_thickness": 0.15, 32 | "fab_text_upright": false, 33 | "other_line_width": 0.15, 34 | "other_text_italic": false, 35 | "other_text_size_h": 1.0, 36 | "other_text_size_v": 1.0, 37 | "other_text_thickness": 0.15, 38 | "other_text_upright": false, 39 | "pads": { 40 | "drill": 0.762, 41 | "height": 1.524, 42 | "width": 1.524 43 | }, 44 | "silk_line_width": 0.15, 45 | "silk_text_italic": false, 46 | "silk_text_size_h": 1.0, 47 | "silk_text_size_v": 1.0, 48 | "silk_text_thickness": 0.15, 49 | "silk_text_upright": false, 50 | "zones": { 51 | "45_degree_only": false, 52 | "min_clearance": 0.508 53 | } 54 | }, 55 | "diff_pair_dimensions": [ 56 | { 57 | "gap": 0.0, 58 | "via_gap": 0.0, 59 | "width": 0.0 60 | } 61 | ], 62 | "drc_exclusions": [], 63 | "meta": { 64 | "version": 2 65 | }, 66 | "rule_severities": { 67 | "annular_width": "error", 68 | "clearance": "error", 69 | "connection_width": "warning", 70 | "copper_edge_clearance": "error", 71 | "copper_sliver": "warning", 72 | "courtyards_overlap": "error", 73 | "diff_pair_gap_out_of_range": "error", 74 | "diff_pair_uncoupled_length_too_long": "error", 75 | "drill_out_of_range": "error", 76 | "duplicate_footprints": "warning", 77 | "extra_footprint": "warning", 78 | "footprint": "error", 79 | "footprint_symbol_mismatch": "warning", 80 | "footprint_type_mismatch": "error", 81 | "hole_clearance": "error", 82 | "hole_near_hole": "error", 83 | "holes_co_located": "warning", 84 | "invalid_outline": "error", 85 | "isolated_copper": "warning", 86 | "item_on_disabled_layer": "error", 87 | "items_not_allowed": "error", 88 | "length_out_of_range": "error", 89 | "lib_footprint_issues": "warning", 90 | "lib_footprint_mismatch": "warning", 91 | "malformed_courtyard": "error", 92 | "microvia_drill_out_of_range": "error", 93 | "missing_courtyard": "ignore", 94 | "missing_footprint": "warning", 95 | "net_conflict": "warning", 96 | "npth_inside_courtyard": "ignore", 97 | "padstack": "error", 98 | "pth_inside_courtyard": "ignore", 99 | "shorting_items": "error", 100 | "silk_edge_clearance": "warning", 101 | "silk_over_copper": "warning", 102 | "silk_overlap": "warning", 103 | "skew_out_of_range": "error", 104 | "solder_mask_bridge": "error", 105 | "starved_thermal": "error", 106 | "text_height": "ignore", 107 | "text_thickness": "warning", 108 | "through_hole_pad_without_hole": "error", 109 | "too_many_vias": "error", 110 | "track_dangling": "warning", 111 | "track_width": "error", 112 | "tracks_crossing": "error", 113 | "unconnected_items": "error", 114 | "unresolved_variable": "error", 115 | "via_dangling": "warning", 116 | "zone_has_empty_net": "error", 117 | "zones_intersect": "error" 118 | }, 119 | "rules": { 120 | "allow_blind_buried_vias": false, 121 | "allow_microvias": false, 122 | "max_error": 0.005, 123 | "min_clearance": 0.0, 124 | "min_connection": 0.0, 125 | "min_copper_edge_clearance": 0.0, 126 | "min_hole_clearance": 0.25, 127 | "min_hole_to_hole": 0.25, 128 | "min_microvia_diameter": 0.2, 129 | "min_microvia_drill": 0.1, 130 | "min_resolved_spokes": 2, 131 | "min_silk_clearance": 0.0, 132 | "min_text_height": 0.8, 133 | "min_text_thickness": 0.08, 134 | "min_through_hole_diameter": 0.3, 135 | "min_track_width": 0.2, 136 | "min_via_annular_width": 0.05, 137 | "min_via_diameter": 0.4, 138 | "solder_mask_clearance": 0.0, 139 | "solder_mask_min_width": 0.0, 140 | "solder_mask_to_copper_clearance": 0.0, 141 | "use_height_for_length_calcs": true 142 | }, 143 | "teardrop_options": [ 144 | { 145 | "td_onpadsmd": true, 146 | "td_onroundshapesonly": false, 147 | "td_ontrackend": false, 148 | "td_onviapad": true 149 | } 150 | ], 151 | "teardrop_parameters": [ 152 | { 153 | "td_allow_use_two_tracks": true, 154 | "td_curve_segcount": 0, 155 | "td_height_ratio": 1.0, 156 | "td_length_ratio": 0.5, 157 | "td_maxheight": 2.0, 158 | "td_maxlen": 1.0, 159 | "td_on_pad_in_zone": false, 160 | "td_target_name": "td_round_shape", 161 | "td_width_to_size_filter_ratio": 0.9 162 | }, 163 | { 164 | "td_allow_use_two_tracks": true, 165 | "td_curve_segcount": 0, 166 | "td_height_ratio": 1.0, 167 | "td_length_ratio": 0.5, 168 | "td_maxheight": 2.0, 169 | "td_maxlen": 1.0, 170 | "td_on_pad_in_zone": false, 171 | "td_target_name": "td_rect_shape", 172 | "td_width_to_size_filter_ratio": 0.9 173 | }, 174 | { 175 | "td_allow_use_two_tracks": true, 176 | "td_curve_segcount": 0, 177 | "td_height_ratio": 1.0, 178 | "td_length_ratio": 0.5, 179 | "td_maxheight": 2.0, 180 | "td_maxlen": 1.0, 181 | "td_on_pad_in_zone": false, 182 | "td_target_name": "td_track_end", 183 | "td_width_to_size_filter_ratio": 0.9 184 | } 185 | ], 186 | "track_widths": [ 187 | 0.0 188 | ], 189 | "tuning_pattern_settings": { 190 | "diff_pair_defaults": { 191 | "corner_radius_percentage": 80, 192 | "corner_style": 1, 193 | "max_amplitude": 1.0, 194 | "min_amplitude": 0.2, 195 | "single_sided": false, 196 | "spacing": 1.0 197 | }, 198 | "diff_pair_skew_defaults": { 199 | "corner_radius_percentage": 80, 200 | "corner_style": 1, 201 | "max_amplitude": 1.0, 202 | "min_amplitude": 0.2, 203 | "single_sided": false, 204 | "spacing": 0.6 205 | }, 206 | "single_track_defaults": { 207 | "corner_radius_percentage": 80, 208 | "corner_style": 1, 209 | "max_amplitude": 1.0, 210 | "min_amplitude": 0.2, 211 | "single_sided": false, 212 | "spacing": 0.6 213 | } 214 | }, 215 | "via_dimensions": [ 216 | { 217 | "diameter": 0.0, 218 | "drill": 0.0 219 | } 220 | ], 221 | "zones_allow_external_fillets": false, 222 | "zones_use_no_outline": true 223 | }, 224 | "ipc2581": { 225 | "dist": "", 226 | "distpn": "", 227 | "internal_id": "", 228 | "mfg": "", 229 | "mpn": "" 230 | }, 231 | "layer_presets": [], 232 | "viewports": [] 233 | }, 234 | "boards": [], 235 | "cvpcb": { 236 | "equivalence_files": [] 237 | }, 238 | "erc": { 239 | "erc_exclusions": [], 240 | "meta": { 241 | "version": 0 242 | }, 243 | "pin_map": [ 244 | [ 245 | 0, 246 | 0, 247 | 0, 248 | 0, 249 | 0, 250 | 0, 251 | 1, 252 | 0, 253 | 0, 254 | 0, 255 | 0, 256 | 2 257 | ], 258 | [ 259 | 0, 260 | 2, 261 | 0, 262 | 1, 263 | 0, 264 | 0, 265 | 1, 266 | 0, 267 | 2, 268 | 2, 269 | 2, 270 | 2 271 | ], 272 | [ 273 | 0, 274 | 0, 275 | 0, 276 | 0, 277 | 0, 278 | 0, 279 | 1, 280 | 0, 281 | 1, 282 | 0, 283 | 1, 284 | 2 285 | ], 286 | [ 287 | 0, 288 | 1, 289 | 0, 290 | 0, 291 | 0, 292 | 0, 293 | 1, 294 | 1, 295 | 2, 296 | 1, 297 | 1, 298 | 2 299 | ], 300 | [ 301 | 0, 302 | 0, 303 | 0, 304 | 0, 305 | 0, 306 | 0, 307 | 1, 308 | 0, 309 | 0, 310 | 0, 311 | 0, 312 | 2 313 | ], 314 | [ 315 | 0, 316 | 0, 317 | 0, 318 | 0, 319 | 0, 320 | 0, 321 | 0, 322 | 0, 323 | 0, 324 | 0, 325 | 0, 326 | 2 327 | ], 328 | [ 329 | 1, 330 | 1, 331 | 1, 332 | 1, 333 | 1, 334 | 0, 335 | 1, 336 | 1, 337 | 1, 338 | 1, 339 | 1, 340 | 2 341 | ], 342 | [ 343 | 0, 344 | 0, 345 | 0, 346 | 1, 347 | 0, 348 | 0, 349 | 1, 350 | 0, 351 | 0, 352 | 0, 353 | 0, 354 | 2 355 | ], 356 | [ 357 | 0, 358 | 2, 359 | 1, 360 | 2, 361 | 0, 362 | 0, 363 | 1, 364 | 0, 365 | 2, 366 | 2, 367 | 2, 368 | 2 369 | ], 370 | [ 371 | 0, 372 | 2, 373 | 0, 374 | 1, 375 | 0, 376 | 0, 377 | 1, 378 | 0, 379 | 2, 380 | 0, 381 | 0, 382 | 2 383 | ], 384 | [ 385 | 0, 386 | 2, 387 | 1, 388 | 1, 389 | 0, 390 | 0, 391 | 1, 392 | 0, 393 | 2, 394 | 0, 395 | 0, 396 | 2 397 | ], 398 | [ 399 | 2, 400 | 2, 401 | 2, 402 | 2, 403 | 2, 404 | 2, 405 | 2, 406 | 2, 407 | 2, 408 | 2, 409 | 2, 410 | 2 411 | ] 412 | ], 413 | "rule_severities": { 414 | "bus_definition_conflict": "error", 415 | "bus_entry_needed": "error", 416 | "bus_label_syntax": "error", 417 | "bus_to_bus_conflict": "error", 418 | "bus_to_net_conflict": "error", 419 | "conflicting_netclasses": "error", 420 | "different_unit_footprint": "error", 421 | "different_unit_net": "error", 422 | "duplicate_reference": "error", 423 | "duplicate_sheet_names": "error", 424 | "endpoint_off_grid": "warning", 425 | "extra_units": "error", 426 | "global_label_dangling": "warning", 427 | "hier_label_mismatch": "error", 428 | "label_dangling": "error", 429 | "lib_symbol_issues": "warning", 430 | "missing_bidi_pin": "warning", 431 | "missing_input_pin": "warning", 432 | "missing_power_pin": "error", 433 | "missing_unit": "warning", 434 | "multiple_net_names": "warning", 435 | "net_not_bus_member": "warning", 436 | "no_connect_connected": "warning", 437 | "no_connect_dangling": "warning", 438 | "pin_not_connected": "error", 439 | "pin_not_driven": "error", 440 | "pin_to_pin": "warning", 441 | "power_pin_not_driven": "error", 442 | "similar_labels": "warning", 443 | "simulation_model_issue": "ignore", 444 | "unannotated": "error", 445 | "unit_value_mismatch": "error", 446 | "unresolved_variable": "error", 447 | "wire_dangling": "error" 448 | } 449 | }, 450 | "libraries": { 451 | "pinned_footprint_libs": [], 452 | "pinned_symbol_libs": [] 453 | }, 454 | "meta": { 455 | "filename": "TCD1304Rev2.kicad_pro", 456 | "version": 1 457 | }, 458 | "net_settings": { 459 | "classes": [ 460 | { 461 | "bus_width": 12, 462 | "clearance": 0.2, 463 | "diff_pair_gap": 0.25, 464 | "diff_pair_via_gap": 0.25, 465 | "diff_pair_width": 0.2, 466 | "line_style": 0, 467 | "microvia_diameter": 0.3, 468 | "microvia_drill": 0.1, 469 | "name": "Default", 470 | "pcb_color": "rgba(0, 0, 0, 0.000)", 471 | "schematic_color": "rgba(0, 0, 0, 0.000)", 472 | "track_width": 0.25, 473 | "via_diameter": 0.8, 474 | "via_drill": 0.4, 475 | "wire_width": 6 476 | }, 477 | { 478 | "bus_width": 12, 479 | "clearance": 0.2, 480 | "diff_pair_gap": 0.25, 481 | "diff_pair_via_gap": 0.25, 482 | "diff_pair_width": 0.2, 483 | "line_style": 0, 484 | "microvia_diameter": 0.3, 485 | "microvia_drill": 0.1, 486 | "name": "Power", 487 | "pcb_color": "rgba(0, 0, 0, 0.000)", 488 | "schematic_color": "rgba(0, 0, 0, 0.000)", 489 | "track_width": 0.35, 490 | "via_diameter": 1.0, 491 | "via_drill": 0.5, 492 | "wire_width": 6 493 | } 494 | ], 495 | "meta": { 496 | "version": 3 497 | }, 498 | "net_colors": null, 499 | "netclass_assignments": null, 500 | "netclass_patterns": [ 501 | { 502 | "netclass": "Power", 503 | "pattern": "+3.3V" 504 | }, 505 | { 506 | "netclass": "Power", 507 | "pattern": "Earth" 508 | } 509 | ] 510 | }, 511 | "pcbnew": { 512 | "last_paths": { 513 | "gencad": "", 514 | "idf": "", 515 | "netlist": "../../../../", 516 | "plot": "Export/Gerbers241226/", 517 | "pos_files": "Export/Gerbers241226/", 518 | "specctra_dsn": "", 519 | "step": "", 520 | "svg": "", 521 | "vrml": "" 522 | }, 523 | "page_layout_descr_file": "" 524 | }, 525 | "schematic": { 526 | "annotate_start_num": 0, 527 | "bom_export_filename": "", 528 | "bom_fmt_presets": [], 529 | "bom_fmt_settings": { 530 | "field_delimiter": ",", 531 | "keep_line_breaks": false, 532 | "keep_tabs": false, 533 | "name": "CSV", 534 | "ref_delimiter": ",", 535 | "ref_range_delimiter": "", 536 | "string_delimiter": "\"" 537 | }, 538 | "bom_presets": [], 539 | "bom_settings": { 540 | "exclude_dnp": false, 541 | "fields_ordered": [ 542 | { 543 | "group_by": false, 544 | "label": "Reference", 545 | "name": "Reference", 546 | "show": true 547 | }, 548 | { 549 | "group_by": true, 550 | "label": "Value", 551 | "name": "Value", 552 | "show": true 553 | }, 554 | { 555 | "group_by": false, 556 | "label": "Datasheet", 557 | "name": "Datasheet", 558 | "show": false 559 | }, 560 | { 561 | "group_by": false, 562 | "label": "Footprint", 563 | "name": "Footprint", 564 | "show": true 565 | }, 566 | { 567 | "group_by": false, 568 | "label": "Qty", 569 | "name": "${QUANTITY}", 570 | "show": true 571 | }, 572 | { 573 | "group_by": true, 574 | "label": "DNP", 575 | "name": "${DNP}", 576 | "show": true 577 | }, 578 | { 579 | "group_by": false, 580 | "label": "#", 581 | "name": "${ITEM_NUMBER}", 582 | "show": false 583 | }, 584 | { 585 | "group_by": false, 586 | "label": "Digikey", 587 | "name": "Digikey", 588 | "show": true 589 | }, 590 | { 591 | "group_by": false, 592 | "label": "MANUFACTURER", 593 | "name": "MANUFACTURER", 594 | "show": false 595 | }, 596 | { 597 | "group_by": false, 598 | "label": "Partnu", 599 | "name": "Partnu", 600 | "show": false 601 | }, 602 | { 603 | "group_by": false, 604 | "label": "Description", 605 | "name": "Description", 606 | "show": true 607 | } 608 | ], 609 | "filter_string": "", 610 | "group_symbols": true, 611 | "name": "", 612 | "sort_asc": true, 613 | "sort_field": "Reference" 614 | }, 615 | "connection_grid_size": 50.0, 616 | "drawing": { 617 | "dashed_lines_dash_length_ratio": 12.0, 618 | "dashed_lines_gap_length_ratio": 3.0, 619 | "default_line_thickness": 6.0, 620 | "default_text_size": 50.0, 621 | "field_names": [], 622 | "intersheets_ref_own_page": false, 623 | "intersheets_ref_prefix": "", 624 | "intersheets_ref_short": false, 625 | "intersheets_ref_show": false, 626 | "intersheets_ref_suffix": "", 627 | "junction_size_choice": 3, 628 | "label_size_ratio": 0.375, 629 | "operating_point_overlay_i_precision": 3, 630 | "operating_point_overlay_i_range": "~A", 631 | "operating_point_overlay_v_precision": 3, 632 | "operating_point_overlay_v_range": "~V", 633 | "overbar_offset_ratio": 1.23, 634 | "pin_symbol_size": 25.0, 635 | "text_offset_ratio": 0.15 636 | }, 637 | "legacy_lib_dir": "", 638 | "legacy_lib_list": [], 639 | "meta": { 640 | "version": 1 641 | }, 642 | "net_format_name": "", 643 | "ngspice": { 644 | "fix_include_paths": true, 645 | "fix_passive_vals": false, 646 | "meta": { 647 | "version": 0 648 | }, 649 | "model_mode": 0, 650 | "workbook_filename": "" 651 | }, 652 | "page_layout_descr_file": "", 653 | "plot_directory": "", 654 | "spice_adjust_passive_values": false, 655 | "spice_current_sheet_as_root": false, 656 | "spice_external_command": "spice \"%I\"", 657 | "spice_model_current_sheet_as_root": true, 658 | "spice_save_all_currents": false, 659 | "spice_save_all_dissipations": false, 660 | "spice_save_all_voltages": false, 661 | "subpart_first_id": 65, 662 | "subpart_id_separator": 0 663 | }, 664 | "sheets": [ 665 | [ 666 | "ed66a1bc-23d1-46cc-9d54-9e49f67ae911", 667 | "Root" 668 | ] 669 | ], 670 | "text_variables": {} 671 | } 672 | -------------------------------------------------------------------------------- /TCD1304Rev2_Python/Accumulators.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | # record structure [ ycols,......, accumulation_counter, datetimestamp ] 4 | 5 | import numpy as np 6 | 7 | class Accumulators: 8 | 9 | def __init__( self, queue, ycol0 = 0, parentinstance = None ): 10 | 11 | self.accumulators = None 12 | self.naccumulators = None 13 | self.counter = 0 14 | 15 | self.queue = queue 16 | self.ycol0 = ycol0 17 | 18 | self.parent = parentinstance 19 | 20 | def __len__(self): 21 | return len(self.accumulators) 22 | 23 | def initialize( self, nrecords ): 24 | self.accumulators = None 25 | self.naccumulators = nrecords 26 | self.counter = 0 27 | return True 28 | 29 | def addrecords( self, newrecords ): 30 | 31 | if len(newrecords) != self.naccumulators: 32 | print( "number of new records != naccumulators" ) 33 | return False 34 | 35 | # First records 36 | if self.accumulators is None: 37 | self.accumulators = newrecords 38 | self.counter = 1 39 | return True 40 | 41 | # Add 42 | for n, (newrecord,oldrecord) in enumerate( zip( newrecords, self.accumulators ) ): 43 | 44 | if isinstance( newrecord[0], list ): 45 | print( 'adding ycols, record', n ) 46 | new_ycols = newrecord[0] 47 | old_ycols = oldrecord[0] 48 | 49 | # ---------------------------------------- 50 | # column 0,1 are time and dac 51 | for m,(newy,oldy) in enumerate( zip(new_ycols[self.ycol0:],old_ycols[self.ycol0:]), start=self.ycol0 ): 52 | old_ycols[m] = (newy + oldy * self.counter)/(self.counter+1) 53 | # ---------------------------------------- 54 | 55 | oldrecord[0] = old_ycols 56 | oldrecord[-2] = self.counter + 1 57 | self.accumulators[n] = oldrecord 58 | 59 | else: 60 | print( 'adding image, record', n ) 61 | new_image = newrecord[0].astype(float) 62 | old_image = oldrecord[0] 63 | 64 | old_image = (new_image + old_image * self.counter)/(self.counter+1) 65 | 66 | oldrecord[0] = old_image 67 | oldrecord[-2] = self.counter + 1 68 | self.accumulators[n] = oldrecord 69 | 70 | 71 | self.counter += 1 72 | print( 'accumulators', len(self.accumulators), 'counter', self.counter ) 73 | 74 | return True 75 | 76 | 77 | def pull( self, checkbusy=True ): 78 | 79 | # Busy collecting data 80 | if checkbusy and self.parent and self.parent.busyflag.value: 81 | print( "busy, try wait" ) 82 | return False 83 | 84 | records = [] 85 | for n in range(self.naccumulators): 86 | try: 87 | newrecord = self.queue.get(timeout=10) 88 | records.append(newrecord) 89 | except: 90 | print( 'add - insufficient records', n, e ) 91 | return False 92 | 93 | if not self.queue.empty(): 94 | print( 'extra records in the queue' ) 95 | return False 96 | 97 | return self.addrecords( records ) 98 | 99 | 100 | def push( self ): 101 | 102 | for record in self.accumulators: 103 | self.queue.put(record) 104 | 105 | # partial init, keep naccumulators 106 | self.accumulators = None 107 | self.counter = 0 108 | 109 | return True 110 | 111 | 112 | def graph( self ): 113 | 114 | if self.parent is None: 115 | print( 'accumualators graph, parent instance is none' ) 116 | return False 117 | 118 | if self.parent.GraphicsWindow is None: 119 | print( 'accumulators parent instance GraphicsWindow is None' ) 120 | return False 121 | 122 | for record in self.accumulators: 123 | self.parent.enqueueGraphics( record ) 124 | 125 | return True 126 | 127 | 128 | def avgvalue( self, colindex, xindices = None ): 129 | 130 | if len( self.accumulators ): 131 | rsum = 0. 132 | for record in self.accumulators: 133 | ycols = record[0] 134 | if xindices is None: 135 | rsum += np.average( ycols[colindex] ) 136 | else: 137 | ycol = ycols[colindex] 138 | rsum += np.average( ycol[xindices] ) 139 | return rsum/len(self.accumulators) 140 | 141 | else: 142 | return 0. 143 | 144 | def avgvalue_above_zero( self, colindex, zero ): 145 | 146 | if len( self.accumulators ): 147 | rsum = 0. 148 | for record in self.accumulators: 149 | ycols = record[0] 150 | ycol = ycols[colindex] 151 | rsum += np.average( ycol[np.where(ycol>zero)] ) 152 | return rsum/len(self.accumulators) 153 | 154 | else: 155 | return 0. 156 | 157 | -------------------------------------------------------------------------------- /TCD1304Rev2_Python/GUIWindow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import os 4 | 5 | from datetime import datetime 6 | 7 | import numpy as np 8 | 9 | from threading import Lock, Semaphore, Thread 10 | from queue import SimpleQueue, Empty 11 | from multiprocessing import Process, Queue, Value 12 | from time import sleep, time, process_time, thread_time 13 | 14 | import matplotlib as mpl 15 | mpl.use("TkAgg") 16 | #mpl.use("TkCairo" ) 17 | 18 | import matplotlib.pyplot as plt 19 | 20 | plt.rcParams['toolbar'] = 'toolmanager' 21 | from matplotlib.backend_tools import ToolBase, ToolToggleBase 22 | 23 | from matplotlib.figure import Figure 24 | from matplotlib.animation import FuncAnimation 25 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg 26 | from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk 27 | #from matplotlib.backends.backend_tkagg import NavigationToolbar2TkAgg as NavigationToolbar2Tk 28 | 29 | from tkinter import ttk 30 | 31 | from tkinter import * 32 | from tkinter.ttk import * 33 | 34 | from tkinter import filedialog 35 | from tkinter import simpledialog 36 | from tkinter import messagebox 37 | 38 | 39 | #from matplotlib.widgets import TextBox 40 | 41 | def transmission_( y, r): 42 | T = np.zeros_like( y ) 43 | idx = np.where( (r>0.) & (y > 0.) ) 44 | T[idx] = y[idx]/r[idx] 45 | return T 46 | 47 | def absorption_( y, r ): 48 | A = np.zeros_like( y ) 49 | idx = np.where( (r>0.) & (y > 0.) ) 50 | A[idx] = np.log10( r[idx]/y[idx] ) 51 | return A 52 | 53 | # ======================================================================================================== 54 | def addtextwidget( fig, box, label, initial, action ): 55 | wax = fig.add_axes( box ) 56 | tbox = TextBox( wax, label, initial=initial, textalignment = 'left' ) 57 | tbox.label.set_color( 'blue' ) 58 | tbox.text_disp.set_color( 'green' ) 59 | tbox.on_submit( action ) 60 | return wax, tbox 61 | # ======================================================================================================== 62 | 63 | def showError( text ): 64 | root = Tk() 65 | root.withdraw() 66 | messagebox.showerror("Error", text) 67 | root.destroy() 68 | 69 | def showWarning( text ): 70 | root = Tk() 71 | root.withdraw() 72 | messagebox.showwarning("Warning", text) 73 | root.destroy() 74 | 75 | def showInfo( text ): 76 | root = Tk() 77 | root.withdraw() 78 | messagebox.showinfo("Info", text) 79 | root.destroy() 80 | 81 | def getfloat( title, prompt, vdef, vmin, vmax ): 82 | root = Tk() 83 | root.withdraw() 84 | result = simpledialog.askfloat( title, prompt, initialvalue=vdef, minvalue=vmin, maxvalue=vmax ) 85 | root.destroy() 86 | return result 87 | 88 | def getinteger( title, prompt, vdef, vmin, vmax ): 89 | root = Tk() 90 | root.withdraw() 91 | result = simpledialog.askinteger( title, prompt, initialvalue=vdef, minvalue=vmin, maxvalue=vmax ) 92 | root.destroy() 93 | return result 94 | 95 | # -------------------------------------------------------------------------- 96 | # Custom tkinter dialog for radiobuttons 97 | def getchoice( title, labels, values, default ): 98 | 99 | root = Tk() 100 | root.title( title ) 101 | root.withdraw() 102 | 103 | idx = values.index(default) 104 | var = StringVar() 105 | var.set(default) 106 | 107 | class ChoiceDialog(simpledialog.Dialog): 108 | def body(self, master): 109 | Label( master, text=title ).pack(anchor='w') 110 | for l,v in zip(labels,values): 111 | print( 'label', l, 'value', v ) 112 | Radiobutton(master, text=l, variable=var, value=v ).pack(anchor='w') 113 | 114 | def apply(self): 115 | self.result = var.get() 116 | print( 'apply', self.result ) 117 | 118 | mode = ChoiceDialog(root) 119 | result = mode.result 120 | print( 'result', result ) 121 | 122 | root.destroy() 123 | 124 | return result 125 | 126 | # -------------------------------------------------------------------------- 127 | # Custom tkinter dialog for settings 128 | 129 | #def getsettings( shutterinterval, frameinterval, framecount, mode, latchmode, processingmode, averaging, parentinstance ): 130 | def getsettings( parent ): 131 | 132 | class SettingsDialog(simpledialog.Dialog): 133 | 134 | def body(self, master ): 135 | 136 | self.clocktrigger = StringVar(master, parent.mode) 137 | self.latchvalue = StringVar(master, parent.latchmode) 138 | self.processingmode = StringVar(master, parent.processingmode) 139 | self.averaging = StringVar(master,parent.averaging) 140 | self.verbosevalue = StringVar(master, str(parent.verbose) ) 141 | 142 | width = 8 143 | row = 0 144 | 145 | self.lclock = Radiobutton(master, text = "Clock", variable = self.clocktrigger, value="clocked" ) 146 | self.lclock.grid(row=row,column=0) 147 | 148 | self.ltrigger = Radiobutton(master, text = "Trigger", variable = self.clocktrigger, value="triggered" ) 149 | self.ltrigger.grid(row=row,column=1) 150 | 151 | self.lgate = Radiobutton(master, text = "Gate", variable = self.clocktrigger, value="gated" ) 152 | self.lgate.grid(row=row,column=2) 153 | 154 | self.clocktrigger.set( parent.mode ) 155 | 156 | row += 1 157 | 158 | self.vsep1 = Separator( master, orient="horizontal" ) 159 | self.vsep1.grid( row=row, column=0,columnspan=3, sticky='ew' ) 160 | 161 | row += 1 162 | 163 | Label( master, text='Shutter', anchor='w' ).grid(row=row,column=0) 164 | Label( master, text=' (usecs)', anchor='w' ).grid(row=row,column=2) 165 | 166 | self.lshutter = Entry(master, width=width ) 167 | self.lshutter.insert( 0, '%d'%parent.shutterinterval ) 168 | self.lshutter.grid(row=row,column=1) 169 | 170 | row += 1 171 | 172 | Label( master, text='Frame', anchor='w' ).grid(row=row,column=0) 173 | Label( master, text=' (usecs)', anchor='w' ).grid(row=row,column=2) 174 | 175 | self.lframe = Entry(master, width=width) 176 | self.lframe.insert( 0, '%d'%parent.frameinterval ) 177 | self.lframe.grid(row=row,column=1) 178 | 179 | row += 1 180 | 181 | Label( master, text='Count', anchor='w' ).grid(row=row,column=0) 182 | Label( master, text=' (frames)', anchor='w' ).grid(row=row,column=2) 183 | 184 | self.lcount = Entry(master, width=width) 185 | self.lcount.insert( 0, '%d'%parent.framecount ) 186 | self.lcount.grid(row=row,column=1) 187 | 188 | row += 1 189 | 190 | self.vsep1 = Separator( master, orient="horizontal" ) 191 | self.vsep1.grid( row=row, column=0,columnspan=3, sticky='ew' ) 192 | 193 | row += 1 194 | 195 | Label( master, text='Averaging', anchor='w' ).grid(row=row,column=0) 196 | Label( master, text=' (framesets)', anchor='w' ).grid(row=row,column=2) 197 | 198 | self.laveraging = Entry(master, width=width) 199 | self.laveraging.insert( 0, '%d'%parent.averaging ) 200 | self.laveraging.grid(row=row,column=1) 201 | 202 | row += 1 203 | 204 | self.vsep1 = Separator( master, orient="horizontal" ) 205 | self.vsep1.grid( row=row, column=0,columnspan=3, sticky='ew' ) 206 | 207 | row += 1 208 | 209 | self.llumin = Radiobutton(master, text = "Raw", variable = self.processingmode, value="emission" ) 210 | self.llumin.grid(row=row,column=0) 211 | 212 | self.labsorp = Radiobutton(master, text = "Absorpt", variable = self.processingmode, value="absorption" ) 213 | self.labsorp.grid(row=row,column=1) 214 | 215 | self.ltransm = Radiobutton(master, text = "Transm", variable = self.processingmode, value="transmission" ) 216 | self.ltransm.grid(row=row,column=2) 217 | 218 | self.processingmode.set( parent.processingmode ) 219 | 220 | row += 1 221 | 222 | self.vsep1 = Separator( master, orient="horizontal" ) 223 | self.vsep1.grid( row=row, column=0,columnspan=3, sticky='ew' ) 224 | 225 | row += 1 226 | 227 | self.lverbose = Checkbutton(master, text = "Verbose", variable = self.verbosevalue, onvalue=str(True), offvalue=str(False) ) 228 | self.lverbose.grid(row=row,column=1) 229 | 230 | self.verbosevalue.set( str(parent.verbose) ) 231 | 232 | def apply(self): 233 | if self.processingmode.get() != "emission" and parent.reference is None: 234 | showError( "set reference first" ) 235 | self.result = False 236 | 237 | else: 238 | try: 239 | self.mode = self.clocktrigger.get() 240 | self.shutterinterval = int( self.lshutter.get() ) 241 | self.frameinterval = int( self.lframe.get() ) 242 | self.framecount = int( self.lcount.get() ) 243 | #self.latchmode = self.latchvalue.get() 244 | self.processingmode = self.processingmode.get() 245 | self.averaging = int(self.laveraging.get()) 246 | self.verbose = eval(self.verbosevalue.get()) 247 | print( 'verbose value', self.verbosevalue.get() ) 248 | self.result = True 249 | except Exception as e: 250 | print(e) 251 | self.result = False 252 | 253 | root = Tk() 254 | root.title( "Settings" ) 255 | root.withdraw() 256 | 257 | dialog = SettingsDialog(root) 258 | result = dialog.result 259 | 260 | if result: 261 | 262 | if dialog.processingmode != parent.processingmode: 263 | print( "processing mode change", dialog.processingmode ) 264 | parent.processingmodechange = True 265 | 266 | parent.mode = dialog.mode 267 | parent.shutterinterval = dialog.shutterinterval 268 | parent.frameinterval = dialog.frameinterval 269 | parent.framecount = dialog.framecount 270 | #parent.latchmode = dialog.latchmode 271 | parent.processingmode = dialog.processingmode 272 | parent.averaging = dialog.averaging 273 | parent.verbose = dialog.verbose 274 | 275 | root.destroy() 276 | 277 | return result 278 | 279 | 280 | # ======================================================================================================== 281 | 282 | class GUIWindow: 283 | 284 | def __init__( self, name, xdata=None, xrange=None, xlabel=None, 285 | ycols=None, yrange=None, ylabels=None, 286 | geometry='1000x600', queue=None, flag=None, nlength=0, ncolumns=0, filespec=None, parentinstance=None, debug = False ): 287 | 288 | # ----------------------- 289 | if parentinstance is None: 290 | raise ValueError('need parent instance') 291 | 292 | if ycols is None and yrange is None: 293 | raise ValueError('need ycols or yrange') 294 | 295 | if ycols is None and (nlength == 0 or ncolumns == 0): 296 | raise ValueError('need ycols or, nlength and ncolumns') 297 | 298 | if filespec is None: 299 | raise ValueError('need initial filespec' ) 300 | 301 | # ----------------------- 302 | # default the initial data 303 | if name is None: 304 | name = 'Data' 305 | self.name = name 306 | 307 | # ----------------------- 308 | # file directory and extension 309 | self.filespec = filespec 310 | self.filedir = os.path.dirname( self.filespec ) 311 | self.fileext = os.path.splitext( self.filespec ) 312 | 313 | # ----------------------- 314 | # parent instrument class instance 315 | self.parentinstance = parentinstance 316 | 317 | self.debug = debug 318 | 319 | if xdata is None: 320 | xdata = np.linspace( 0, nlength, nlength ) 321 | self.xdata = xdata 322 | 323 | if xlabel is None: 324 | xlabel = 'index' 325 | self.xlabel = xlabel 326 | 327 | self.xrange = xrange 328 | 329 | if ycols is None: 330 | ycols = [np.zeros(nlength)] * ncolumns 331 | self.ycols = ycols 332 | 333 | if ylabels is None: 334 | ylabels = [ 'Chan %d' for d in range(ncolumns) ] 335 | self.ylabels = ylabels 336 | 337 | self.yrange = yrange 338 | 339 | self.geometry = geometry 340 | 341 | if queue is None: 342 | self.queue = Queue() 343 | else: 344 | self.queue = queue 345 | 346 | if flag is None: 347 | self.flag = Value('i',1) 348 | else: 349 | self.flag = flag 350 | self.flag.value = 1 351 | 352 | self.thread = None 353 | 354 | self.history = [] 355 | self.historypointer = 0 356 | 357 | self.reference = None 358 | 359 | # gui controls 360 | self.shutterinterval = 10000 361 | self.frameinterval = 10000 362 | self.framecount = 1 363 | self.fastshutter = False 364 | self.mode = 'clocked' 365 | self.latchmode = 'nolatch' 366 | self.processingmode = 'emission' 367 | self.averaging = 0 368 | 369 | self.processingmodechange = False 370 | self.autorun = False 371 | 372 | self.verbose = False 373 | 374 | def animation( self, interval = 200, blit = True ): 375 | 376 | self.fig = plt.figure(self.name) 377 | if self.geometry is not None: 378 | dpi = self.fig.get_dpi() 379 | width,height = self.geometry.lower().split('x',maxsplit=1) 380 | self.fig.set_size_inches( int(width)/dpi, int(height)/dpi ) 381 | 382 | self.fig.subplots_adjust(top=0.9) 383 | 384 | # This gives us scrolling through the history record 385 | self.fig.canvas.manager.toolmanager.add_tool('RunStart', self.RunStart, graphobj=self ) 386 | self.fig.canvas.manager.toolmanager.add_tool('STOP', self.RunStop, graphobj=self ) 387 | self.fig.canvas.manager.toolmanager.add_tool('Settings', self.Settings, graphobj=self ) 388 | self.fig.canvas.manager.toolmanager.add_tool('Prev', self.PreviousGraph, graphobj=self ) 389 | self.fig.canvas.manager.toolmanager.add_tool('Next', self.NextGraph, graphobj=self ) 390 | self.fig.canvas.manager.toolmanager.add_tool('Last', self.LastGraph, graphobj=self ) 391 | self.fig.canvas.manager.toolmanager.add_tool('Set', self.StoreReference, graphobj=self ) 392 | self.fig.canvas.manager.toolmanager.add_tool('Clr', self.ClearReference, graphobj=self ) 393 | self.fig.canvas.manager.toolmanager.add_tool('Save', self.SaveData, graphobj=self ) 394 | self.fig.canvas.manager.toolbar.add_tool('RunStart', 'toolgroup0',-1) 395 | self.fig.canvas.manager.toolbar.add_tool('STOP', 'toolgroup0',-1) 396 | self.fig.canvas.manager.toolbar.add_tool('Settings', 'toolgroup1',-1) 397 | self.fig.canvas.manager.toolbar.add_tool('Prev', 'toolgroup2',-1) 398 | self.fig.canvas.manager.toolbar.add_tool('Next', 'toolgroup2',-1) 399 | self.fig.canvas.manager.toolbar.add_tool('Last', 'toolgroup2',-1) 400 | self.fig.canvas.manager.toolbar.add_tool('Set', 'toolgroup3',-1) 401 | self.fig.canvas.manager.toolbar.add_tool('Clr', 'toolgroup3',-1) 402 | self.fig.canvas.manager.toolbar.add_tool('Save', 'toolgroup4',-1) 403 | 404 | self.ax = self.fig.add_subplot(1, 1, 1) 405 | 406 | self.lns = [] 407 | for y, label in zip( self.ycols, self.ylabels ): 408 | ln = self.ax.plot( self.xdata, y, label=label ) 409 | ln = ln[0] 410 | #print( 'ax.plot returned', ln ) 411 | self.lns.append(ln) 412 | self.ylabels.append(label) 413 | 414 | self.txt = self.ax.text( 0.99, 0.99, '0', horizontalalignment='right', verticalalignment='top', transform=self.ax.transAxes ) 415 | 416 | self.ax.legend( loc='upper left' ) 417 | 418 | if self.xrange: 419 | self.ax.set_xlim( self.xrange ) 420 | 421 | if self.yrange: 422 | self.ax.set_ylim( self.yrange ) 423 | 424 | # still need plt.show() to launch it. 425 | if self.queue is not None: 426 | self.ani = FuncAnimation(self.fig, self.animation_update, interval=200, blit=blit ) 427 | 428 | self.fig.canvas.mpl_connect('close_event', self.close ) 429 | 430 | plt.show() 431 | 432 | plt.close() 433 | 434 | def animation_update( self, i ): 435 | 436 | if self.flag.value: 437 | 438 | gotstuff = False 439 | 440 | while True: 441 | try: 442 | record = self.queue.get(block=False) 443 | 444 | self.graphrecord_( record ) 445 | 446 | self.history.append(record) 447 | if len(self.history) > 100: 448 | self.history.pop(0) 449 | self.historypointer = len(self.history)-1 450 | 451 | gotstuff = True 452 | #print( 'got record' ) 453 | except Empty: 454 | break 455 | 456 | if self.autorun and gotstuff: 457 | self.autorun = self.autocommand() 458 | 459 | 460 | else: 461 | self.ani.event_source.stop() 462 | plt.close() 463 | 464 | #return tuple( self.lns) + ( self.txt, ) 465 | return (self.txt, *self.lns ) 466 | 467 | def start( self, interval=200, blit=True ): 468 | 469 | self.thread = Process( target=self.animation,args=(interval,blit) ) 470 | self.thread.start() 471 | 472 | def close( self, ignored=None ): 473 | 474 | self.flag.value = 0 475 | 476 | if self.thread: 477 | try: 478 | self.thread.terminate() 479 | self.thread.join() 480 | except Exception as e: 481 | pass 482 | 483 | def autocommand( self ): 484 | 485 | if self.mode == "triggered": 486 | s = "trigger %d %d"%(max(self.framecount,1),self.shutterinterval) 487 | 488 | elif self.mode == "gated": 489 | s = "gate 1" 490 | 491 | else: 492 | s = "read %d"%(self.shutterinterval) 493 | 494 | return self.command( s ) 495 | 496 | def command( self, s, clear=True, accumulate=False ): 497 | 498 | if clear: 499 | self.parentinstance.clear() 500 | 501 | if accumulate: 502 | self.parentinstance.accumulatorflag.value = 1 503 | else: 504 | self.parentinstance.accumulatorflag.value = 0 505 | 506 | retv = True 507 | 508 | if self.verbose: 509 | print( s ) 510 | 511 | self.parentinstance.write( s + '\n' ) 512 | 513 | response = self.parentinstance.read() 514 | 515 | if response is not None: 516 | for r in response: 517 | if r.startswith( "Error" ): 518 | print( s ) 519 | print( r ) 520 | showError( r.strip() ) 521 | retv = False 522 | 523 | return retv 524 | 525 | # --------------------------------------------------------- 526 | def graphupdate( self, xdata=None, ycols=None, text=None ): 527 | 528 | if ycols is not None: 529 | for ln, y in zip(self.lns, ycols): 530 | ln.set_ydata( y ) 531 | 532 | if xdata is not None: 533 | for ln in self.lns: 534 | ln.set_xdata( xdata ) 535 | self.ax.set_xlim( left=min(xdata), right=max(xdata) ) 536 | self.ax.relim() 537 | self.ax.autoscale_view() 538 | 539 | if text is not None: 540 | self.txt.set_text( text ) 541 | 542 | return ( self.txt, *self.lns ) 543 | 544 | # --------------------------------------------------------- 545 | # --------------------------------------------------------- 546 | def graphrecord_( self, record ): 547 | 548 | ycols, text = record 549 | 550 | if len(ycols) > 1: 551 | 552 | x = ycols[0] 553 | 554 | if self.processingmode == "absorption": 555 | if self.verbose: 556 | print( "converting to absorption", "new mode", self.processingmodechange ) 557 | yrefs, textrefs = self.reference 558 | for n, (y, r) in enumerate( ycols[1:], yrefs[1:] ): 559 | y = absorption_( y, r ) 560 | self.lns[n].set_data( x, y ) 561 | 562 | if self.processingmodechange: 563 | print( 'setting y axis' ) 564 | self.processingmodechange = False 565 | self.ax.set_ylim( -0.01, 2.0 ) 566 | 567 | elif self.processingmode == "transmission" and self.reference is not None: 568 | if self.verbose: 569 | print( "converting to transmission", "new mode", self.processingmodechange ) 570 | yrefs, textrefs = self.reference 571 | for n, (y, r) in enumerate( ycols[1:], yrefs[1:] ): 572 | y = transmission_( y, r ) 573 | self.lns[n].set_data( x, y ) 574 | 575 | if self.processingmodechange: 576 | print( 'setting y axis' ) 577 | self.processingmodechange = False 578 | self.ax.set_ylim( 0, 1.0 ) 579 | 580 | else: 581 | if self.verbose: 582 | print( "raw emission", "new mode", self.processingmodechange ) 583 | 584 | for n, y in enumerate( ycols[1:] ): 585 | self.lns[n].set_data( x, y ) 586 | 587 | if self.processingmodechange: 588 | print( 'setting y axis' ) 589 | self.processingmodechange = False 590 | self.ax.set_ylim( self.yrange ) 591 | 592 | self.ax.set_xlim( left=min(x), right=max(x) ) 593 | self.ax.relim() 594 | self.ax.autoscale_view() 595 | 596 | elif len(ycols) == 1: 597 | 598 | if self.processingmode == "absorption": 599 | if self.verbose: 600 | print( "converting to absorption" ) 601 | 602 | yrefs, textrefs = self.reference 603 | y = absorption_( ycols[0], yrefs[0] ) 604 | self.lns[0].set_ydata( y ) 605 | 606 | if self.processingmodechange: 607 | print( 'setting y axis absorption' ) 608 | self.processingmodechange = False 609 | self.ax.set_ylim( -0.01, 2.0 ) 610 | self.ax.relim() 611 | self.ax.autoscale_view() 612 | self.fig.canvas.draw() 613 | 614 | elif self.processingmode == "transmission": 615 | if self.verbose: 616 | print( "converting to transmission" ) 617 | 618 | yrefs, textrefs = self.reference 619 | y = transmission_( ycols[0], yrefs[0] ) 620 | self.lns[0].set_ydata( y ) 621 | 622 | if self.processingmodechange: 623 | print( 'setting y axis transmission' ) 624 | self.processingmodechange = False 625 | self.ax.set_ylim( 0, 1.0 ) 626 | self.ax.relim() 627 | self.ax.autoscale_view() 628 | self.fig.canvas.draw() 629 | 630 | else: 631 | self.lns[0].set_ydata( ycols[0] ) 632 | 633 | if self.processingmodechange: 634 | print( 'setting y axis, raw' ) 635 | self.processingmodechange = False 636 | self.ax.set_ylim( self.yrange ) 637 | self.ax.relim() 638 | self.ax.autoscale_view() 639 | self.fig.canvas.draw() 640 | 641 | self.txt.set_text( text ) 642 | 643 | class LastGraph(ToolBase): 644 | default_keymap = 'up' 645 | description = 'last data vector' 646 | 647 | def __init__(self, *args, **kwargs): 648 | self.graph = kwargs.pop('graphobj') 649 | super().__init__(*args, **kwargs) 650 | #ToolBase.__init__(self, *args, **kwargs) 651 | 652 | def trigger(self, *args, **kwargs): 653 | #print('last graph key pressed') 654 | 655 | try: 656 | record = self.graph.history[-1] 657 | self.graph.historypointer = len(self.graph.history)-1 658 | 659 | self.graph.graphrecord_( record ) 660 | self.graph.fig.canvas.draw() 661 | except Exception as e: 662 | print("LastGraph", e ) 663 | 664 | class NextGraph(ToolBase): 665 | default_keymap = 'right' 666 | description = 'next data vector' 667 | 668 | def __init__(self, *args, **kwargs): 669 | self.graph = kwargs.pop('graphobj') 670 | super().__init__(*args, **kwargs) 671 | #ToolBase.__init__(self, *args, **kwargs) 672 | 673 | def trigger(self, *args, **kwargs): 674 | #print('next graph key pressed') 675 | 676 | if self.graph.historypointer < len(self.graph.history) - 1: 677 | 678 | self.graph.historypointer += 1 679 | record = self.graph.history[self.graph.historypointer] 680 | 681 | self.graph.graphrecord_( record ) 682 | self.graph.fig.canvas.draw() 683 | else: 684 | print( 'already at last graph' ) 685 | 686 | class PreviousGraph(ToolBase): 687 | default_keymap = 'left' 688 | description = 'prev data vector' 689 | 690 | def __init__(self, *args, **kwargs): 691 | self.graph = kwargs.pop('graphobj') 692 | super().__init__(*args, **kwargs) 693 | #ToolBase.__init__(self, *args, **kwargs) 694 | 695 | def trigger(self, *args, **kwargs): 696 | #print('previous graph key pressed') 697 | 698 | if self.graph.historypointer > 0: 699 | 700 | self.graph.historypointer -= 1 701 | record = self.graph.history[self.graph.historypointer] 702 | 703 | self.graph.graphrecord_( record ) 704 | self.graph.fig.canvas.draw() 705 | else: 706 | print( 'already at first graph' ) 707 | 708 | class StoreReference(ToolBase): 709 | description = 'store last graphed spectrum as reference' 710 | 711 | def __init__(self, *args, **kwargs): 712 | self.graph = kwargs.pop('graphobj') 713 | super().__init__(*args, **kwargs) 714 | #ToolBase.__init__(self, *args, **kwargs) 715 | 716 | def trigger(self, *args, **kwargs): 717 | #print('previous graph key pressed') 718 | 719 | if self.graph.historypointer > 0: 720 | 721 | record = self.graph.history[self.graph.historypointer] 722 | self.graph.reference = record 723 | 724 | else: 725 | print( 'no data available' ) 726 | 727 | class ClearReference(ToolBase): 728 | description = 'clear reference spectrum' 729 | 730 | def __init__(self, *args, **kwargs): 731 | self.graph = kwargs.pop('graphobj') 732 | super().__init__(*args, **kwargs) 733 | #ToolBase.__init__(self, *args, **kwargs) 734 | 735 | def trigger(self, *args, **kwargs): 736 | #print('previous graph key pressed') 737 | 738 | self.graph.reference = None 739 | 740 | class SaveData(ToolBase): 741 | default_keymap = 'enter' 742 | description = 'save data to disk' 743 | 744 | def __init__(self, *args, **kwargs): 745 | self.graph = kwargs.pop('graphobj') 746 | super().__init__(*args, **kwargs) 747 | #ToolBase.__init__(self, *args, **kwargs) 748 | 749 | def trigger(self, *args, **kwargs): 750 | #print('previous graph key pressed') 751 | 752 | #initialfile = "tdaqdata." + timestamp.strftime('%Y%m%d.%H%M%S.%f') + self.graph.parentinstance.filesuffix 753 | timestamp = datetime.now() 754 | fext = '.' + timestamp.strftime('%Y%m%d.%H%M%S.%f') + self.graph.parentinstance.filesuffix 755 | 756 | root = Tk() 757 | root.withdraw() 758 | fname = filedialog.asksaveasfilename( 759 | title='Save the data', 760 | defaultextension=fext, 761 | initialdir='.' 762 | ) 763 | root.destroy() 764 | 765 | if fname in ["", ()]: 766 | return 767 | 768 | fname = str(fname) 769 | print( fname ) 770 | 771 | try: 772 | self.graph.parentinstance.savetofile( fname, 773 | timestamp=None, 774 | comments = None, 775 | write_ascii=True, 776 | framesetindex = None, 777 | records = None ) 778 | 779 | except Exception as e: 780 | messagebox.showerror("Error saving file", fname + " " + str(e)) 781 | 782 | # --------------------------------------------------------- 783 | class Settings(ToolBase): 784 | 785 | description = 'set shutter, frame interval and count' 786 | 787 | def __init__(self, *args, **kwargs): 788 | self.graph = kwargs.pop('graphobj') 789 | super().__init__(*args, **kwargs) 790 | #ToolBase.__init__(self, *args, **kwargs) 791 | 792 | def trigger(self, *args, **kwargs): 793 | 794 | ''' 795 | result = getsettings(self.graph.shutter,self.graph.frameinterval,self.graph.framecount, 796 | self.graph.mode, self.graph.latchmode, self.graph.processingmode, self.graph.averaging, self.graph ) 797 | ''' 798 | result = getsettings( self.graph ) 799 | print( result ) 800 | 801 | 802 | class RunStart(ToolBase): 803 | description = 'start data acquisition' 804 | 805 | def __init__(self, *args, **kwargs): 806 | self.graph = kwargs.pop('graphobj') 807 | super().__init__(*args, **kwargs) 808 | #ToolBase.__init__(self, *args, **kwargs) 809 | 810 | def trigger(self, *args, **kwargs): 811 | 812 | if self.graph.framecount <= 0: 813 | 814 | if not self.graph.autorun: 815 | self.graph.autorun = self.graph.autocommand() 816 | else: 817 | self.graph.autorun = False 818 | 819 | elif self.graph.mode == "triggered": 820 | 821 | if self.graph.averaging > 1: 822 | s = "trigger %d %d %d"%( self.graph.averaging, 823 | max(self.graph.framecount,1), 824 | self.graph.shutterinterval) 825 | 826 | self.graph.command( s, accumulate=True ) 827 | 828 | else: 829 | s = "trigger %d %d"%( max(self.graph.framecount,1), 830 | self.graph.shutterinterval) 831 | 832 | self.graph.command( s ) 833 | 834 | elif self.graph.mode == "gated": 835 | 836 | if self.graph.averaging > 1: 837 | s = "gate %d"%(self.graph.averaging) 838 | 839 | self.graph.command( s, accumulate=True ) 840 | 841 | else: 842 | s = "gate %d"%( max(self.graph.framecount,1) ) 843 | 844 | self.graph.command( s ) 845 | 846 | elif self.graph.shutterinterval == self.graph.frameinterval: 847 | 848 | if self.graph.averaging > 1: 849 | s = "read %d %d"%(self.graph.averaging, 850 | self.graph.shutterinterval) 851 | 852 | self.graph.command( s, accumulate=True ) 853 | 854 | else: 855 | s = "read %d %d"%( max(self.graph.framecount,1), 856 | self.graph.shutterinterval) 857 | 858 | self.graph.command( s ) 859 | 860 | elif self.graph.averaging > 1: 861 | 862 | s = "read %d %d %d"%(self.graph.averaging, 863 | self.graph.shutterinterval, 864 | self.graph.frameinterval) 865 | 866 | self.graph.command( s, accumulate=True ) 867 | 868 | else: 869 | s = "read %d %d %d"%( max(self.graph.framecount,1), 870 | self.graph.shutterinterval, 871 | self.graph.frameinterval) 872 | 873 | self.graph.command( s ) 874 | 875 | class RunStop(ToolBase): 876 | 877 | description = 'stop data acquisition' 878 | 879 | def __init__(self, *args, **kwargs): 880 | self.graph = kwargs.pop('graphobj') 881 | super().__init__(*args, **kwargs) 882 | #ToolBase.__init__(self, *args, **kwargs) 883 | 884 | def trigger(self, *args, **kwargs): 885 | 886 | self.graph.autorun = False 887 | 888 | self.graph.command( 'stop' ) 889 | -------------------------------------------------------------------------------- /TCD1304Rev2_Python/GraphicsWindow.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | import numpy as np 4 | 5 | from threading import Lock, Semaphore, Thread 6 | from queue import SimpleQueue, Empty 7 | from multiprocessing import Process, Queue, Value 8 | from time import sleep, time, process_time, thread_time 9 | 10 | import matplotlib as mpl 11 | #mpl.use("TkAgg") 12 | #mpl.use("TkCairo" ) 13 | 14 | import matplotlib.pyplot as plt 15 | 16 | plt.rcParams['toolbar'] = 'toolmanager' 17 | from matplotlib.backend_tools import ToolBase, ToolToggleBase 18 | 19 | from matplotlib.figure import Figure 20 | from matplotlib.animation import FuncAnimation 21 | from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg 22 | from matplotlib.backends.backend_tkagg import NavigationToolbar2Tk 23 | #from matplotlib.backends.backend_tkagg import NavigationToolbar2TkAgg as NavigationToolbar2Tk 24 | 25 | 26 | class GraphicsWindow: 27 | 28 | def __init__( self, name, xdata=None, xrange=None, xlabel=None, 29 | ycols=None, yrange=None, ylabels=None, 30 | geometry='800x600', queue=None, flag=None, nlength=0, ncolumns=0, debug = False ): 31 | 32 | # ----------------------- 33 | # default the initial data 34 | if name is None: 35 | name = 'Data' 36 | self.name = name 37 | 38 | self.debug = debug 39 | 40 | if xdata is None: 41 | xdata = np.linspace( 0, nlength, nlength ) 42 | self.xdata = xdata 43 | 44 | if xlabel is None: 45 | xlabel = 'index' 46 | self.xlabel = xlabel 47 | 48 | self.xrange = xrange 49 | 50 | if ycols is None: 51 | ycols = [np.zeros(nlength)] * ncolumns 52 | self.ycols = ycols 53 | 54 | if ylabels is None: 55 | ylabels = [ 'Chan %d' for d in range(ncolumns) ] 56 | self.ylabels = ylabels 57 | 58 | self.yrange = yrange 59 | 60 | self.geometry = geometry 61 | 62 | if queue is None: 63 | self.queue = Queue() 64 | else: 65 | self.queue = queue 66 | 67 | if flag is None: 68 | self.flag = Value('i',1) 69 | else: 70 | self.flag = flag 71 | self.flag.value = 1 72 | 73 | self.thread = None 74 | 75 | self.history = [] 76 | self.historypointer = 0 77 | 78 | def animation( self, interval = 200, blit = True ): 79 | 80 | self.fig = plt.figure(self.name) 81 | if self.geometry is not None: 82 | dpi = self.fig.get_dpi() 83 | width,height = self.geometry.lower().split('x',maxsplit=1) 84 | self.fig.set_size_inches( int(width)/dpi, int(height)/dpi ) 85 | 86 | self.fig.subplots_adjust(top=0.9) 87 | 88 | # This gives us scrolling through the history record 89 | self.fig.canvas.manager.toolmanager.add_tool('Prev', self.PreviousGraph, graphobj=self ) 90 | self.fig.canvas.manager.toolmanager.add_tool('Next', self.NextGraph, graphobj=self ) 91 | self.fig.canvas.manager.toolmanager.add_tool('Last', self.LastGraph, graphobj=self ) 92 | self.fig.canvas.manager.toolbar.add_tool('Prev', 'toolgroup', -1) 93 | self.fig.canvas.manager.toolbar.add_tool('Next', 'toolgroup', -1) 94 | self.fig.canvas.manager.toolbar.add_tool('Last', 'toolgroup', -1) 95 | 96 | self.ax = self.fig.add_subplot(1, 1, 1) 97 | 98 | self.lns = [] 99 | for y, label in zip( self.ycols, self.ylabels ): 100 | ln = self.ax.plot( self.xdata, y, label=label ) 101 | ln = ln[0] 102 | #print( 'ax.plot returned', ln ) 103 | self.lns.append(ln) 104 | self.ylabels.append(label) 105 | 106 | self.txt = self.ax.text( 0.99, 0.99, '0', horizontalalignment='right', verticalalignment='top', transform=self.ax.transAxes ) 107 | 108 | self.ax.legend( loc='upper left' ) 109 | 110 | if self.xrange: 111 | self.ax.set_xlim( self.xrange ) 112 | 113 | if self.yrange: 114 | self.ax.set_ylim( self.yrange ) 115 | 116 | # still need plt.show() to launch it. 117 | if self.queue is not None: 118 | self.ani = FuncAnimation(self.fig, self.animation_update, interval=200, blit=blit ) 119 | 120 | self.fig.canvas.mpl_connect('close_event', self.close ) 121 | 122 | plt.show() 123 | 124 | plt.close() 125 | 126 | def animation_update( self, i ): 127 | 128 | if self.flag.value: 129 | while True: 130 | try: 131 | record = self.queue.get(block=False) 132 | 133 | self.graphrecord_( record ) 134 | 135 | self.history.append(record) 136 | if len(self.history) > 100: 137 | self.history.pop(0) 138 | self.historypointer = len(self.history)-1 139 | 140 | #print( 'got record' ) 141 | except Empty: 142 | break 143 | 144 | else: 145 | self.ani.event_source.stop() 146 | plt.close() 147 | 148 | #return tuple( self.lns) + ( self.txt, ) 149 | return (self.txt, *self.lns ) 150 | 151 | 152 | def start( self, interval=200, blit=True ): 153 | 154 | self.thread = Process( target=self.animation,args=(interval,blit) ) 155 | self.thread.start() 156 | 157 | def close( self, ignored=None ): 158 | 159 | self.flag.value = 0 160 | 161 | if self.thread: 162 | try: 163 | self.thread.terminate() 164 | self.thread.join() 165 | except Exception as e: 166 | pass 167 | 168 | # --------------------------------------------------------- 169 | # graph (verb) the passed record 170 | def graphrecord_( self, record ): 171 | 172 | ycols, text = record 173 | 174 | if len(ycols) > 1: 175 | 176 | x = ycols[0] 177 | 178 | for n, y in enumerate( ycols[1:] ): 179 | self.lns[n].set_data( x, y ) 180 | 181 | self.ax.set_xlim( left=min(x), right=max(x) ) 182 | self.ax.relim() 183 | self.ax.autoscale_view() 184 | 185 | elif len(ycols) == 1: 186 | 187 | self.lns[0].set_ydata( ycols[0] ) 188 | 189 | 190 | self.txt.set_text( text ) 191 | 192 | 193 | # History button graph functions 194 | class LastGraph(ToolBase): 195 | default_keymap = 'up' 196 | description = 'last data vector' 197 | 198 | def __init__(self, *args, **kwargs): 199 | self.graph = kwargs.pop('graphobj') 200 | super().__init__(*args, **kwargs) 201 | #ToolBase.__init__(self, *args, **kwargs) 202 | 203 | def trigger(self, *args, **kwargs): 204 | #print('last graph key pressed') 205 | 206 | try: 207 | record = self.graph.history[-1] 208 | self.graph.historypointer = len(self.graph.history)-1 209 | 210 | self.graph.graphrecord_( record ) 211 | self.graph.fig.canvas.draw() 212 | except Exception as e: 213 | print("LastGraph", e ) 214 | 215 | 216 | class NextGraph(ToolBase): 217 | default_keymap = 'right' 218 | description = 'next data vector' 219 | 220 | def __init__(self, *args, **kwargs): 221 | self.graph = kwargs.pop('graphobj') 222 | super().__init__(*args, **kwargs) 223 | #ToolBase.__init__(self, *args, **kwargs) 224 | 225 | def trigger(self, *args, **kwargs): 226 | #print('next graph key pressed') 227 | 228 | if self.graph.historypointer < len(self.graph.history) - 1: 229 | 230 | self.graph.historypointer += 1 231 | record = self.graph.history[self.graph.historypointer] 232 | 233 | self.graph.graphrecord_( record ) 234 | self.graph.fig.canvas.draw() 235 | else: 236 | print( 'already at last graph' ) 237 | 238 | class PreviousGraph(ToolBase): 239 | default_keymap = 'left' 240 | description = 'prev data vector' 241 | 242 | def __init__(self, *args, **kwargs): 243 | self.graph = kwargs.pop('graphobj') 244 | super().__init__(*args, **kwargs) 245 | #ToolBase.__init__(self, *args, **kwargs) 246 | 247 | 248 | def trigger(self, *args, **kwargs): 249 | #print('previous graph key pressed') 250 | 251 | if self.graph.historypointer > 0: 252 | 253 | self.graph.historypointer -= 1 254 | record = self.graph.history[self.graph.historypointer] 255 | 256 | self.graph.graphrecord_( record ) 257 | self.graph.fig.canvas.draw() 258 | else: 259 | print( 'already at first graph' ) 260 | 261 | # --------------------------------------------------------- 262 | def graphupdate( self, xdata=None, ycols=None, text=None ): 263 | 264 | if ycols is not None: 265 | for ln, y in zip(self.lns, ycols): 266 | ln.set_ydata( y ) 267 | 268 | if xdata is not None: 269 | for ln in self.lns: 270 | ln.set_xdata( xdata ) 271 | self.ax.set_xlim( left=min(xdata), right=max(xdata) ) 272 | self.ax.relim() 273 | self.ax.autoscale_view() 274 | 275 | if text is not None: 276 | self.txt.set_text( text ) 277 | 278 | return ( self.txt, *self.lns ) 279 | 280 | -------------------------------------------------------------------------------- /TCD1304Rev2_Python/TCD1304Rev2Controller.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python 2 | 3 | """ 4 | TCD1304Rev2Controller.py 5 | 6 | Mitchell C. Nelson (c) 2023 7 | 8 | November 26, 2023 9 | 10 | Derived from LLCDController.py Copyright 2022 by M C Nelson 11 | Derived from TDAQSerial.py, TCD1304Serial.py, Copyrigh 2021 by M C Nelson 12 | 13 | """ 14 | 15 | __author__ = "Mitchell C. Nelson, PhD" 16 | __copyright__ = "Copyright 2023, Mitchell C, Nelson" 17 | __version__ = "0.3" 18 | __email__ = "drmcnelson@gmail.com" 19 | __status__ = "alpha testing" 20 | 21 | __all__ = [ 'LCCDFRAME', 'LCCDDATA', 'LCCDDATASET' ] 22 | 23 | versionstring = 'LCCDController.py - version %s %s M C Nelson, PhD, (c) 2023'%(__version__,__status__) 24 | 25 | import sys 26 | import time 27 | import select 28 | 29 | import os 30 | import operator 31 | 32 | import signal 33 | import atexit 34 | 35 | #from timeit import default_timer as timer 36 | 37 | import platform 38 | 39 | import serial 40 | 41 | from datetime import datetime 42 | from time import sleep, time, process_time, thread_time 43 | 44 | # from threading import Lock, Semaphore, Thread 45 | from queue import SimpleQueue, Empty 46 | from multiprocessing import Process, Queue, Value, Lock 47 | 48 | import struct 49 | 50 | import inspect 51 | from itertools import count 52 | 53 | import regex 54 | from pyparsing import nestedExpr 55 | 56 | import numpy as np 57 | from scipy.signal import savgol_filter as savgol 58 | 59 | import matplotlib.pyplot as plt 60 | 61 | try: 62 | from Accumulators import Accumulators 63 | has_accumulators = True 64 | except ModuleNotFoundError: 65 | has_accumulators = False 66 | 67 | try: 68 | from TextWindow import TextWindow 69 | has_TextWindow = True 70 | except Exception as e: 71 | has_TextWindow = False 72 | 73 | try: 74 | from GUIWindow import GUIWindow 75 | has_GUIWindow = True 76 | except Exception as e: 77 | has_GUIWindow = False 78 | print(e) 79 | 80 | try: 81 | from GraphicsWindow import GraphicsWindow 82 | has_GraphicsWindow = True 83 | except: 84 | has_GraphicsWindow = False 85 | 86 | # ---------------------------------------------------------- 87 | 88 | def lineno(): 89 | return inspect.currentframe().f_back.f_lineno 90 | 91 | def errorprint(*s): 92 | print(*s, file = sys.stderr) 93 | 94 | def input_ready(): 95 | return (sys.stdin in select.select([sys.stdin], [], [], 0)[0]) 96 | 97 | def key_in_list( ls, key, mapf=str ): 98 | try: 99 | idx = ls.index(key) 100 | return mapf( ls[idx+1] ) 101 | except: 102 | return None 103 | 104 | def generate_x_vector( npoints, coefficients = None ): 105 | 106 | x = np.linspace( 0, npoints, npoints ) 107 | 108 | if coefficients is None: 109 | return x 110 | 111 | else: 112 | return np.polynomial.polynomial.polyval( x, coefficients ) 113 | 114 | # -------------------------------------------------------------------------------------------- 115 | def split_nested( s ): 116 | result = regex.search(r''' 117 | (? #capturing group rec 118 | \( #open parenthesis 119 | (?: #non-capturing group 120 | [^()]++ #anyting but parenthesis one or more times without backtracking 121 | | #or 122 | (?&rec) #recursive substitute of group rec 123 | )* 124 | \) #close parenthesis 125 | ) 126 | ''',s) 127 | return result 128 | 129 | #matches = [match.group() for match in regex.finditer(r"(?:(\((?>[^()]+|(?1))*\))[\S)+",s)] 130 | #return matches 131 | 132 | def split_bracketed(string, delimiter=' ', strip_brackets=False): 133 | """ Split a string by the delimiter unless it is inside brackets. 134 | e.g. 135 | list(bracketed_split('abc,(def,ghi),jkl', delimiter=',')) == ['abc', '(def,ghi)', 'jkl'] 136 | 137 | stackoverflow question 21662474, answer by Peter, 10/5/21 138 | """ 139 | 140 | openers = '[{(<' 141 | closers = ']})>' 142 | opener_to_closer = dict(zip(openers, closers)) 143 | opening_bracket = dict() 144 | current_string = '' 145 | depth = 0 146 | for c in string: 147 | if c in openers: 148 | depth += 1 149 | opening_bracket[depth] = c 150 | if strip_brackets and depth == 1: 151 | continue 152 | elif c in closers: 153 | assert depth > 0, f"You exited more brackets than we have entered in string {string}" 154 | assert c == opener_to_closer[opening_bracket[depth]], \ 155 | f"Closing bracket {c} did not match opening bracket {opening_bracket[depth]} in string {string}" 156 | depth -= 1 157 | if strip_brackets and depth == 0: 158 | continue 159 | if depth == 0 and c == delimiter: 160 | yield current_string 161 | current_string = '' 162 | else: 163 | current_string += c 164 | assert depth == 0, f'You did not close all brackets in string {string}' 165 | yield current_string 166 | 167 | # ====================================================================================================================== 168 | # ====================================================================================================================== 169 | # Classes for reading data from disk file 170 | 171 | # This is the individual frame, the framesets class has a list of frames 172 | 173 | class LCCDFRAME: 174 | 175 | def __init__( self, content, parent, offsetdigits=6, dtype=None ): 176 | 177 | self.parent = parent 178 | 179 | self.accumulate = 0 180 | 181 | while len(content): 182 | 183 | line = content[0].lower().strip() 184 | if not len(line): 185 | content = content[1:] 186 | continue 187 | 188 | # We found the data 189 | if line.lower().startswith( '# data ascii' ): 190 | self.datalen = int(line.split()[3] ) 191 | data = [] 192 | n = 1 193 | for l in content[1:]: 194 | n += 1 195 | if l.lower().startswith( '# end data' ): 196 | break 197 | data.append( float( l ) ) 198 | self.data = np.array(data) 199 | content = content[n:] 200 | #print( 'completed data', len(data) ) 201 | 202 | # Frame header and supplemental (after the data, until the blank lines) 203 | elif line[0] == '#': 204 | 205 | line = line[1:].strip() 206 | 207 | if '=' in line: 208 | exec( line, self.__dict__ ) 209 | 210 | # We simply load the dictionary from the keyword value pairs in the frame header 211 | elif line.lower().startswith( 'timestamp' ): 212 | key,value = line.split(maxsplit=1) 213 | elif key == 'adcdata': 214 | self.__dict__[key] = [ a for a in map( float, value.split() ) ] 215 | 216 | content = content[1:] 217 | 218 | else: 219 | print( 'Warning: empty line in frame' ) 220 | 221 | content = content[1:] 222 | 223 | for key in ['INTERVAL','CLOCK','MODE','SETS','ACCUMULATE','OUTERCOUNTER', 224 | 'interval','clock','mode','sets','accumulate','outercounter']: 225 | if key in self.__dict__ and key not in parent.__dict__: 226 | parent.__dict__[key] = self.__dict__[key] 227 | 228 | if 'ELAPSED' in self.__dict__: 229 | self.__dict__['offset'] = self.ELAPSED/1.E6 230 | elif 'elapsed' in self.__dict__: 231 | self.__dict__['offset'] = self.elapsed/1.E6 232 | elif 'TRIGGERELAPSED' in self.__dict__: 233 | self.__dict__['ELAPSED'] = self.TRIGGERELAPSED 234 | self.__dict__['offset'] = self.TRIGGERELAPSED/1.E6 235 | elif 'tiggerelapsed' in self.__dict__: 236 | self.__dict__['triggerelapsed'] = self.triggerelapsed 237 | self.__dict__['offset'] = self.triggerelapsed/1.E6 238 | 239 | if 'offset' in self.__dict__ and offsetdigits: 240 | self.offset = round( self.offset,offsetdigits) 241 | 242 | def __len__(self): 243 | return len(self.data) 244 | 245 | def get(self,s): 246 | if s in self.__dict__: 247 | return self.__dict__[s] 248 | else: 249 | exec( 'retv='+s, self.__dict__ ) 250 | return retv 251 | 252 | # ------------------------------------------------------------------ 253 | def dump(self): 254 | for key,val in self.__dict__.items(): 255 | if key == 'parent' and val is not None: 256 | print( key, self.parent.filename ) 257 | elif key != '__builtins__': 258 | print( key, val ) 259 | # ------------------------------------------------------------------ 260 | 261 | 262 | # --------------------------------------------------------------------------------------------- 263 | # --------------------------------------------------------------------------------------------- 264 | 265 | class LCCDDATA: 266 | 267 | def __init__( self, filename, dtype=None, relatedobject=None, offsetdigits=6, verbose=False ): 268 | 269 | self.filename = filename 270 | self.relatedobject = relatedobject 271 | 272 | with open(filename,'r') as f: 273 | content = f.read().splitlines() 274 | 275 | if not content[0].startswith( "# LCCD" ): 276 | print( content[0] ) 277 | raise ValueError( "file is not LCCD" ); 278 | 279 | # Read the header 280 | self.version = content[0] 281 | self.filedate = content[1] 282 | self.chiptemperature = None 283 | self.coefficients = None 284 | 285 | for n,line in enumerate(content[2:],start=2): 286 | 287 | if line[0] != '#': 288 | raise ValueError( 'lines in file header need to start with #' ) 289 | 290 | line = line[1:].strip() 291 | 292 | if line.lower().startswith("header end" ): 293 | content = content[n+1:] 294 | break 295 | 296 | # =============================== 297 | # these are the version 0 files 298 | if line.startswith( "shutter" ): 299 | content = content[n:] 300 | break 301 | # ============================== 302 | 303 | if '=' in line: 304 | exec( line, self.__dict__ ) 305 | 306 | # ============================== 307 | # version 0 parameter specfications 308 | else: 309 | if ':' in line: 310 | key,value = line.split(':',maxsplit=1) 311 | else: 312 | key,value = line.split(maxsplit=1) 313 | 314 | if key == 'coefficients': 315 | self.coefficients = [ a for a in map( float, value.split() ) ] 316 | else: 317 | try: 318 | self.__dict__[key] = int( value ) 319 | except ValueError: 320 | try: 321 | self.__dict__[key] = float( value ) 322 | except ValueError: 323 | # this is the last resort, it if fails, need to fix the file 324 | self.__dict__[key] = str( value ) 325 | 326 | # ------------------------------------------ 327 | # Post header fixups 328 | if 'datalength' in self.__dict__: 329 | self.xdata = generate_x_vector( self.datalength, self.coefficients ) 330 | self.wavelengths = self.xdata 331 | self.pixels = np.linspace( 0, self.datalength, self.datalength ) 332 | 333 | # ========================================== 334 | # Load the frames 335 | self.frames = [] 336 | 337 | while( len(content) ): 338 | 339 | # Skip blank lines 340 | for n, line in enumerate(content): 341 | line = line.strip() 342 | if len(line): 343 | break 344 | content = content[n:] 345 | if not len(content): 346 | break 347 | 348 | # Find next blank lines 349 | for m, line in enumerate(content): 350 | line = line.strip() 351 | if len(line) == 0: 352 | break 353 | if not len(content[:m]): 354 | break 355 | 356 | frame = LCCDFRAME(content[:m],self,offsetdigits) 357 | if len(frame): 358 | self.frames.append(frame) 359 | else: 360 | raise ValueError('empty frame') 361 | 362 | content = content[m:] 363 | 364 | # ------------------------------------------ 365 | # Adjust offsets for pulse leadin 366 | if 'pulse_leadin' in self.__dict__ : 367 | offset = self.frames[self.pulse_leadin].offset; 368 | for f in self.frames: 369 | f.offset -= offset 370 | if offsetdigits: 371 | f.offset = round(f.offset,offsetdigits) 372 | 373 | def __len__(self): 374 | return len(self.frames) 375 | 376 | def get(self,s): 377 | if s in self.__dict__: 378 | return self.__dict__[s] 379 | else: 380 | exec( 'retv='+s, self.__dict__ ) 381 | return retv 382 | 383 | def getlist( self, attr_name ): 384 | 385 | rets = [] 386 | for f in self.dataset: 387 | rets.append( f.get(attr_name) ) 388 | 389 | try: 390 | rtemp = np.array(rets) 391 | rets = rtemp 392 | except: 393 | pass 394 | 395 | return rets 396 | 397 | # ------------------------------------------------------------------ 398 | def dump(self): 399 | print( "***************************" ) 400 | for key,val in self.__dict__.items(): 401 | if key == 'relatedobject' and val is not None: 402 | if 'filename' in self.relatedobject.__dict__: 403 | print( key, self.relatedobject.filename ) 404 | 405 | elif key not in ['__builtins__','frames']: 406 | print( key, val ) 407 | 408 | for n,frame in enumerate(self.frames): 409 | print( "---------------------------" ) 410 | print( "*frame", n ) 411 | frame.dump() 412 | 413 | if self.relatedobject is not None: 414 | try: 415 | self.relatedobject.dump() 416 | except Exception as e: 417 | print(e) 418 | 419 | # --------------------------------------------------------------------------------------------- 420 | # --------------------------------------------------------------------------------------------- 421 | class LCCDDATASET: 422 | 423 | def __init__( self, filespecs=None, dataset=None, sort_attr=None, verbose=False ): 424 | 425 | self.dataset = [] 426 | 427 | if filespecs is not None: 428 | 429 | if type(filespecs) is not list: 430 | filespecs = [ filespecs ] 431 | 432 | for filespec in filespecs: 433 | 434 | for filespec_ in glob.glob(filespec): 435 | 436 | if filespec_.endswith('lccd'): 437 | lccd = LCCDDATA( filespec_, verbose=verbose ) 438 | self.dataset.append(lccd) 439 | 440 | if dataset is not None: 441 | for d in dataset: 442 | if isinstance(d,LCCDDATA): 443 | self.dataset.append(d) 444 | else: 445 | raise ValueError( 'not LCCDDATA instance' ) 446 | 447 | if sort_attr is not None: 448 | self.dataset.sort( key=operator.attrgetter( sort_attr ) ) 449 | 450 | def get(self,s): 451 | if s in self.__dict__: 452 | return self.__dict__[s] 453 | else: 454 | exec( 'retv='+s, self.__dict__ ) 455 | return retv 456 | 457 | def getlist( self, attr_name ): 458 | 459 | rets = [] 460 | for d in self.dataset: 461 | rets.append( d.get(attr_name) ) 462 | 463 | try: 464 | rtemp = np.array(rets) 465 | rets = rtemp 466 | except: 467 | pass 468 | 469 | return rets 470 | 471 | def sort( self, attr_name ): 472 | print( 'sort', attr_name ) 473 | self.dataset.sort( key=operator.attrgetter( attr_name ) ) 474 | 475 | 476 | def slices( self, n, currentnorm=False ): 477 | 478 | slices_ = [] 479 | for d in self.dataset: 480 | if currentnorm: 481 | slices_.append( d.frames[n].data/d.relatedobject.avgcurrent ) 482 | else: 483 | slices_.append( d.frames[n].data ) 484 | 485 | return np.array(slices_) 486 | 487 | def __len__(self): 488 | return len(self.dataset) 489 | 490 | # ====================================================================================================================== 491 | # ====================================================================================================================== 492 | # This is the class for the instrument 493 | 494 | class LCCDCONTROLLER: 495 | 496 | _ids = count(0) 497 | 498 | def __init__( self, portspec, readtimeout=1., writetimeout=1., monitor=True, graphics=True, 499 | graph_by_pixels=False, xrange=None, yrange=None, graph_ylabel='spectrum', 500 | coefficients=None, gui=False, 501 | debug=False ): 502 | 503 | 504 | if gui and not has_GUIWindow: 505 | raise ValueError( "GUI requested, but GUIWindow.py not loaded" ) 506 | 507 | if graphics and not has_GraphicsWindow: 508 | raise ValueError( "Graphics requested, but GraphicsWindow.py not loaded" ) 509 | 510 | if monitor and not has_TextWindow: 511 | raise ValueError( "Monitor requested, but TextWindow.py not loaded." ) 512 | 513 | # ------------------------------------------------------------------ 514 | self.ser = serial.Serial( portspec, timeout=readtimeout, write_timeout=writetimeout ) 515 | 516 | self.instance = next( self._ids ) 517 | 518 | self.name= portspec 519 | 520 | self.textqueue = Queue() 521 | self.dataqueue = Queue() 522 | 523 | self.monitorthread = None 524 | self.monitorWindow = None 525 | self.monitorqueue = Queue() 526 | 527 | self.GrapichsWindow = None 528 | self.xdata = None 529 | 530 | self.flag = Value( 'i', 1 ) 531 | self.busyflag = Value( 'i', 0 ) 532 | self.accumulatorflag = Value( 'i', 0 ) 533 | 534 | # Local accumulators for the command line 535 | self.accumulators = Accumulators( self.dataqueue, ycol0=0, parentinstance = self ) 536 | 537 | # Control data processing in reader 538 | self.baselineflag = Value( 'i', 1 ) 539 | 540 | # Report errors back 541 | self.errorflag = Value( 'i', 0 ) 542 | 543 | self.bits = 12 544 | self.vfs = 3.3 545 | self.vperbit = self.vfs/(2**self.bits - 1) 546 | 547 | # -------------------------------- 548 | self.filesuffix = ".lccd" 549 | 550 | self.identifier = None 551 | self.coefficients = [] 552 | 553 | self.graph_by_pixels = graph_by_pixels 554 | 555 | # --------------------------------- 556 | 557 | self.debug = debug 558 | 559 | # --------------------------------- 560 | # First, stop 561 | buffer = self.rawcommand( 'stop' ); 562 | if buffer is not None: 563 | print( buffer ) 564 | 565 | # --------------------------------- 566 | # Query for Identifier 567 | buffer = self.rawcommand( 'identifier', 'Identifier' ); 568 | if buffer is not None: 569 | print( buffer ) 570 | self.identifier = buffer.split(maxsplit=1)[1] 571 | 572 | # --------------------------------- 573 | # Query for the configuration 574 | buffer = self.rawcommand( 'configuration', 'PIXELS' ); 575 | if buffer is None: 576 | raise ValueError( "configuration, not found in response" ) 577 | print( buffer ) 578 | 579 | parts = buffer.split() 580 | self.datalength = key_in_list( parts, "PIXELS", int ) 581 | self.darklength = key_in_list( parts, "DARK", int ) 582 | self.invert = key_in_list( parts, "INVERT", float ) 583 | self.sensor = key_in_list( parts, "SENSOR", str ) 584 | 585 | if "VPERBIT" in parts: 586 | self.vperbit = key_in_list( parts, "VPERBIT", float ) 587 | print( "Vperbit", self.vperbit ) 588 | 589 | if "BITS" in parts and "VFS" in parts : 590 | self.bits = key_in_list( parts, "BITS", int ) 591 | self.vfs = key_in_list( parts, "VFS", float ) 592 | self.vperbit = self.vfs/(2**self.bits - 1) 593 | print( "BITS", self.bits, "VFS", self.vfs, "Vperbit", self.vperbit ) 594 | 595 | print( "pixels", self.datalength, 596 | "dark", self.darklength, 597 | "invert", self.invert, 598 | "vperbit", self.vperbit, 599 | "sensor", self.sensor ) 600 | 601 | # ---------------------------------------- 602 | # Query for coefficients 603 | 604 | self.xdata = np.linspace( 0, self.datalength, self.datalength ) 605 | self.xlabel = 'Pixels' 606 | 607 | if coefficients is not None: 608 | print( 'using specified coefficients', self.coefficients ) 609 | self.coefficients = coefficients 610 | self.xlabel = 'user coords' 611 | 612 | else: 613 | buffer = self.rawcommand( 'coefficients', 'coefficients' ); 614 | if buffer is not None: 615 | if self.debug: 616 | print( 'coefficients buffer', buffer ) 617 | # Also sets self.xdata 618 | if self.parsecoefficients( buffer ): 619 | self.xlabel = 'Wavelength' 620 | print( 'coefficients', self.coefficients ) 621 | print( self.xdata ) 622 | 623 | 624 | # --------------------------------- 625 | if xrange is None: 626 | xrange = (self.xdata[0],self.xdata[-1]) 627 | if yrange is None: 628 | yrange = (-self.vfs/20,self.vfs) 629 | 630 | if gui: 631 | # We will want to query these from the device 632 | self.GraphicsWindow = GUIWindow( "LCCDController Data " + portspec, 633 | xdata = self.xdata, 634 | xlabel = self.xlabel, 635 | ycols = [ np.zeros(self.datalength) ], 636 | yrange = (-self.vfs/20,self.vfs), 637 | ylabels = [graph_ylabel], 638 | flag = self.flag, 639 | parentinstance = self, 640 | filespec = os.path.join( 'datafile', self.filesuffix ), 641 | debug = self.debug ) 642 | self.GraphicsWindow.start( ) 643 | 644 | elif graphics: 645 | # We will want to query these from the device 646 | self.GraphicsWindow = GraphicsWindow( "LCCDController Data " + portspec, 647 | xdata = self.xdata, 648 | xlabel = 'wavelength', 649 | ycols = [ np.zeros(self.datalength) ], 650 | yrange = (-self.vfs/20,self.vfs), 651 | ylabels = [graph_ylabel], 652 | flag = self.flag, 653 | debug = self.debug ) 654 | self.GraphicsWindow.start( ) 655 | 656 | if monitor: 657 | self.monitorthread = Process( target = self.textmonitor, args=(portspec, self.flag ) ) 658 | self.monitorthread.start() 659 | 660 | self.readerthread = Process( target = self.reader, 661 | args=(portspec, self.flag, self.busyflag, self.accumulatorflag, 662 | self.baselineflag, 663 | self.errorflag, self.debug ) ) 664 | self.readerthread.start() 665 | 666 | atexit.register(self.exit, None ) 667 | 668 | # ------------------------------------------ 669 | def parsecoefficients( self, buffer ): 670 | 671 | print( buffer ) 672 | if 'nan' in buffer: 673 | self.coefficients = [ 0., 1., 0., 0. ] 674 | self.xdata = generate_x_vector( self.datalength, None ) 675 | return False 676 | 677 | try: 678 | self.coefficients = [ a for a in map( float, buffer.split()[1:] ) ] 679 | self.xdata = generate_x_vector( self.datalength, self.coefficients ) 680 | except Exception as e: 681 | print( e ) 682 | return False 683 | 684 | return True 685 | 686 | # =========================================== 687 | def rawcommand( self, command, key=None ): 688 | 689 | print( "sending", command ) 690 | self.write( command + '\n' ); 691 | 692 | response = [] 693 | while True: 694 | try: 695 | buffer = self.ser.read_until( ) 696 | buffer = buffer.decode()[:-1] 697 | print( "rcvd: ", buffer ) 698 | except Exception as e: 699 | print( "rawread", e ) 700 | break 701 | if buffer.startswith( "DONE" ): 702 | break 703 | response.append( buffer ) 704 | 705 | if key is not None: 706 | print( 'rawcommand scanning response for ', key ) 707 | # return the selected line or None 708 | candidate = None 709 | for s in response: 710 | print( 'rawcommand line:', s ) 711 | if s.startswith( key ): 712 | candidate = s 713 | if candidate is not None: 714 | return candidate 715 | print( key, ' not found in response', response ) 716 | return None 717 | 718 | return response 719 | 720 | # =========================================== 721 | def exit( self, ignored=None ): 722 | print( self.name + ' exit()' ) 723 | self.write( 'stop\n' ); 724 | self.close() 725 | 726 | def busy(self): 727 | return self.busyflag.value 728 | 729 | # Wait for completion of data frames 730 | def wait(self, timeout=None, interruptible=False ): 731 | 732 | if timeout is None: 733 | while self.busyflag.value: 734 | if interruptible and input_ready(): 735 | return False 736 | sleep( 0.2 ) 737 | return True 738 | 739 | try: 740 | timeout = float(timeout) 741 | except: 742 | print( "not valid timeout value", timeout ) 743 | return False 744 | 745 | while self.busyflag.value: 746 | if interruptible and input_ready(): 747 | return False 748 | elif timeout > 0.: 749 | sleep( 0.2 ) 750 | timeout -= 0.2 751 | else: 752 | return False 753 | 754 | return True 755 | 756 | # =========================================== 757 | def textmonitor( self, name, flag ): 758 | 759 | print( "text monitor started ", name ) 760 | 761 | self.monitorWindow = TextWindow("LCCDSpectrometer Log " + name) 762 | print( self.monitorWindow ) 763 | 764 | def on_closing(): 765 | print( 'closed' ) 766 | flag.value = 0 767 | #self.monitorWindow.parent.protocol("WM_DELETE_WINDOW", on_closing) 768 | self.monitorWindow.parent.protocol("WM_DELETE_WINDOW", self.monitorWindow.parent.iconify) 769 | 770 | def sighandler( a, b ): 771 | flag.value = 0 772 | signal.signal(signal.SIGINT, sighandler) 773 | 774 | while flag.value: 775 | try: 776 | line = self.monitorqueue.get(block=False) 777 | self.monitorWindow.addline_( line ) 778 | except Empty: 779 | pass 780 | self.monitorWindow.update() 781 | sleep(0.1) 782 | self.monitorWindow.close() 783 | 784 | # ======================================= 785 | def enqueueGraphics(self, record ): 786 | 787 | ycols, \ 788 | frame_shutter, frame_interval, frame_clock, \ 789 | frame_counter, frame_outercounter, \ 790 | frame_frames, frame_sets, frame_every, \ 791 | frame_mode, frame_elapsed, \ 792 | accumulate, timestamp = record 793 | 794 | text = frame_mode + ' ' + str(frame_shutter) + ' ' + str(frame_interval) + '/' + str(frame_clock) 795 | text += '\ncounters: ' + str(frame_counter) + '/' + str(frame_frames) 796 | text += ' ' + str(frame_outercounter) + str(frame_sets) 797 | text += '\n' + timestamp.strftime('%Y-%m-%d.%H%M%S.%f') 798 | if accumulate: 799 | text += '\n' + str(accumulate) 800 | 801 | #print( "enqueue graphics" ) 802 | self.GraphicsWindow.queue.put( [ ycols, text ] ) 803 | #self.GraphicsWindow.queue.put( record ) 804 | 805 | return True 806 | 807 | 808 | def reader( self, name, flag, busyflag, accumulatorflag, baselineflag, errorflag, debug=False ): 809 | 810 | # ---------------------------------------------- 811 | def sighandler( a, b ): 812 | print( "reader sighandler" ) 813 | flag.value = 0 814 | 815 | signal.signal(signal.SIGINT, sighandler) 816 | 817 | # ---------------------------------------------- 818 | naccumulators = 0 819 | accumulators = [] 820 | accumulation_counters = [] 821 | accumulators_elapsed = [] 822 | 823 | # Need fast access to these, do the "." now 824 | datalength = self.datalength 825 | dataqueue_get = self.dataqueue.get 826 | dataqueue_put = self.dataqueue.put 827 | dataqueue_empty = self.dataqueue.empty 828 | 829 | graphics_put = self.GraphicsWindow.queue.put 830 | 831 | def initializeaccumulators(): 832 | nonlocal accumulators 833 | nonlocal accumulation_counters 834 | nonlocal accumulators_elapsed 835 | nonlocal naccumulators 836 | nonlocal datalength 837 | 838 | print( "setting up accumulators", frame_frames ) 839 | naccumulators = frame_frames 840 | accumulators = [ np.zeros(datalength) ] * naccumulators 841 | accumulation_counters = [ 0 ] * naccumulators 842 | accumulators_elapsed = [0 ] * naccumulators 843 | 844 | def enqueueaccumulators(): 845 | nonlocal accumulators 846 | nonlocal accumulation_counters 847 | nonlocal accumulators_elapsed 848 | nonlocal naccumulators 849 | 850 | for nindex,(data,counter,elapsed) in enumerate( 851 | zip(accumulators,accumulation_counters,accumulators_elapsed) ): 852 | 853 | if counter > 1: 854 | data /= counter 855 | 856 | record = [ [data], 857 | frame_shutter, frame_interval, frame_clock, 858 | nindex+1, frame_outercounter, 859 | frame_frames, frame_sets, frame_every, 860 | frame_mode, frame_elapsed, 861 | counter, timestamp ] 862 | 863 | dataqueue_put( record ) 864 | 865 | def recordprocessing( ): 866 | nonlocal accumulators 867 | nonlocal accumulation_counters 868 | nonlocal accumulators_elapsed 869 | nonlocal naccumulators 870 | nonlocal data 871 | 872 | if accumulate: 873 | 874 | nindex = (frame_counter - 1) 875 | 876 | print( 'accumulating index', nindex, naccumulators ) 877 | counter = accumulation_counters[nindex ] 878 | 879 | accumulators[nindex] += data 880 | accumulation_counters[nindex] += 1 881 | 882 | oldelapsed = accumulators_elapsed[nindex] 883 | accumulators_elapsed[nindex] = frame_elapsed 884 | 885 | if accumulation_counters[nindex] > 1: 886 | data = accumulators[nindex] / accumulation_counters[nindex] 887 | if oldelapsed != frame_elapsed: 888 | print( "Warning: elapsed times differ", oldelapsed, frame_elapsed ) 889 | 890 | else: 891 | record = [ [data], 892 | frame_shutter, frame_interval, frame_clock, 893 | frame_counter, frame_outercounter, 894 | frame_frames, frame_sets, frame_every, 895 | frame_mode, frame_elapsed, 896 | 0, timestamp ] 897 | 898 | dataqueue_put( record ) 899 | 900 | # ---------------------------------------- 901 | if self.GraphicsWindow: 902 | text = frame_mode 903 | text += ' ' + str(frame_shutter) 904 | text += ' ' + str(frame_interval) 905 | text += '/' + str(frame_clock) 906 | text += ' ' + str(frame_elapsed) 907 | text += '\ncounters: ' 908 | text += str(frame_counter) + '/' + str(frame_frames) 909 | text += ' ' + str(frame_outercounter) + '/' + str(frame_sets) 910 | text += '\n' + timestamp.strftime('%Y-%m-%d.%H%M%S.%f') 911 | if accumulate: 912 | text += '\n' + str(accumulation_counters[nindex]) 913 | 914 | #print( "enqueue graphics" ) 915 | graphics_put( [ [data], text ] ) 916 | 917 | def initialize_recordprocessing(): 918 | 919 | if debug: 920 | print( 'lccd setting busyflag' ) 921 | busyflag.value = 1 922 | try: 923 | while not dataqueue_empty(): 924 | dataqueue_get() 925 | except Exception as e: 926 | print( 'clearing dataqueue', e ) 927 | 928 | if accumulatorflag.value: 929 | initializeaccumulators() 930 | accumulate = True 931 | 932 | # ------------------------------------------------- 933 | frame_mode = "" 934 | frame_elapsed = 0 935 | frame_interval = 0 936 | frame_shutter = 0 937 | frame_clock = 0 938 | frame_counter = 0 939 | frame_outercounter = 0 940 | frame_frames = 0 941 | frame_every = 0 942 | frame_sets = 0 943 | frameset_complete = False 944 | 945 | accumulate = False 946 | accumulate_counter = 0 947 | 948 | frameset_complete = False 949 | 950 | def initialize_parameters(): 951 | nonlocal frame_elapsed 952 | nonlocal frame_interval 953 | nonlocal frame_shutter 954 | nonlocal frame_clock 955 | nonlocal frame_counter 956 | nonlocal frame_outercounter 957 | nonlocal frame_frames 958 | nonlocal frame_every 959 | nonlocal frame_sets 960 | nonlocal frameset_complete 961 | frame_elapsed = 0 962 | frame_interval = 0 963 | frame_shutter = 0 964 | frame_clock = 0 965 | frame_counter = 0 966 | frame_outercounter = 0 967 | frame_frames = 0 968 | frame_every = 0 969 | frame_sets = 0 970 | frameset_complete = False 971 | 972 | 973 | # ---------------------------------------------- 974 | print( "lccd reader start", name, 'debug', debug ) 975 | 976 | read_until = self.ser.read_until 977 | 978 | while flag.value: 979 | 980 | buffer = read_until( ) 981 | 982 | if buffer is not None and len(buffer) >1 and flag.value: 983 | 984 | try: 985 | buffer = buffer.decode()[:-1] 986 | except: 987 | print( 'failed decode', buffer ) 988 | continue 989 | 990 | if debug: 991 | print( "lccd reader: ", buffer ) 992 | 993 | if buffer.startswith( "DONE" ): 994 | self.textqueue.put( buffer ) 995 | 996 | # Receive Ascii Formatted Data 997 | elif buffer.startswith( "DATA" ): 998 | ndata = int(buffer[4:]) 999 | #print( 'ndata', ndata ) 1000 | 1001 | # Read the actual text format data buffer(s) 1002 | data_buffers = [] 1003 | while len(data_buffers) < ndata: 1004 | data_buffers.append( self.ser.read_until( ) ) 1005 | 1006 | timestamp = datetime.now() 1007 | 1008 | # Read(Expect) the end of data message 1009 | endbuffer = self.ser.read_until( ) 1010 | endbuffer = endbuffer.decode()[:-1] 1011 | 1012 | if debug: 1013 | print( "lccd endbuffer: ", endbuffer ) 1014 | 1015 | # If valid, process the data 1016 | if endbuffer.startswith( "END" ): 1017 | 1018 | data = [] 1019 | for b in data_buffers: 1020 | data.append( int(b.decode()) ) 1021 | 1022 | data = np.array(data) 1023 | 1024 | if self.vperbit: 1025 | data = data * self.vperbit 1026 | 1027 | if baselineflag.value and self.darklength: 1028 | data -= np.sum( data[:self.darklength] ) / self.darklength 1029 | 1030 | # ---------------------------------------- 1031 | recordprocessing() 1032 | # ---------------------------------------- 1033 | 1034 | else: 1035 | print('reader ' + name + ' ', buffer, len(data), ' without END') 1036 | self.textqueue.put( "ERROR: data not completed" ) 1037 | if self.monitorquue: 1038 | self.monitorqueue.put( "ERROR: data not completed\n" ) 1039 | 1040 | # -------------------------------- 1041 | # Receive Binary Formatted Data Buffer 1042 | elif buffer.startswith( "BINARY16" ): 1043 | ndata = int(buffer[8:]) 1044 | #print( 'ndata', ndata ) 1045 | 1046 | # Read the data 1047 | data = self.ser.read( ndata*2 ) 1048 | 1049 | timestamp = datetime.now() 1050 | 1051 | # Read(Expect) the end of data message 1052 | endbuffer = self.ser.read_until( ) 1053 | endbuffer = endbuffer.decode()[:-1] 1054 | 1055 | # Update the text display, begin and end mesages 1056 | if debug: 1057 | print( "endbuffer: ", endbuffer ) 1058 | 1059 | if endbuffer.startswith( "END" ): 1060 | 1061 | data = struct.unpack( '<%dH'%(len(data)/2), data ) 1062 | 1063 | data = np.array(data) 1064 | 1065 | if self.vperbit: 1066 | data = data * self.vperbit 1067 | 1068 | if baselineflag.value and self.darklength: 1069 | data -= np.sum( data[:self.darklength] ) / self.darklength 1070 | 1071 | #data = self._mapdata( data ) 1072 | 1073 | # ---------------------------------------- 1074 | recordprocessing() 1075 | # ---------------------------------------- 1076 | 1077 | else: 1078 | print('reader ' + name + ' ', buffer, len(data), ' without END') 1079 | 1080 | self.textqueue.put( "ERROR: data not completed" ) 1081 | if self.monitorqueue: 1082 | self.monitorqueue.put( "ERROR: data not completed\n" ) 1083 | 1084 | # =================================================== 1085 | # Command flags 1086 | elif buffer.startswith( "ELAPSED" ): 1087 | 1088 | try: 1089 | frame_elapsed = int(buffer[7:]) 1090 | except Exception as e: 1091 | print( buffer, e ) 1092 | self.errorflag.value += 1 1093 | self.textqueue.put( 'Error: ' + buffer ) 1094 | 1095 | elif buffer.startswith( "COUNTER" ): 1096 | 1097 | #print( buffer ) 1098 | try: 1099 | frame_counter = int(buffer[7:]) 1100 | except Exception as e: 1101 | print( buffer, e ) 1102 | self.errorflag.value += 1 1103 | self.textqueue.put( 'Error: ' + buffer ) 1104 | 1105 | elif buffer.startswith( "SHUTTER" ): 1106 | #print( buffer ) 1107 | try: 1108 | frame_shutter = int(buffer[8:]) 1109 | except Exception as e: 1110 | print( buffer, e ) 1111 | self.errorflag.value += 1 1112 | self.textqueue.put( 'Error: ' + buffer ) 1113 | 1114 | # --------------------------------------- 1115 | # This come at the start and end of each frameset 1116 | elif buffer.startswith( "FRAMESET START" ): 1117 | frameset_complete = False 1118 | try: 1119 | frame_outercounter = int(buffer[14:]) 1120 | except Exception as e: 1121 | print( buffer, e ) 1122 | self.errorflag.value += 1 1123 | self.textqueue.put( 'Error: ' + buffer ) 1124 | 1125 | elif buffer.startswith( "FRAMESET END" ): 1126 | frameset_complete = True 1127 | 1128 | # --------------------------------------- 1129 | # Common specs 1130 | # note, we need the space after keyword for this one 1131 | elif buffer.startswith( "CLOCK " ): 1132 | #print( buffer ) 1133 | try: 1134 | frame_clock = int(buffer[5:]) 1135 | except Exception as e: 1136 | print( buffer, e ) 1137 | self.errorflag.value += 1 1138 | self.textqueue.put( 'Error: ' + buffer ) 1139 | 1140 | elif buffer.startswith( "INTERVAL" ): 1141 | #print( buffer ) 1142 | try: 1143 | frame_interval = int(buffer[8:]) 1144 | except Exception as e: 1145 | print( buffer, e ) 1146 | self.errorflag.value += 1 1147 | self.textqueue.put( 'Error: ' + buffer ) 1148 | 1149 | elif buffer.startswith( "FRAMES" ): 1150 | #print( buffer ) 1151 | try: 1152 | frame_frames = int(buffer[6:]) 1153 | except Exception as e: 1154 | print( buffer, e ) 1155 | self.errorflag.value += 1 1156 | self.textqueue.put( 'Error: ' + buffer ) 1157 | 1158 | elif buffer.startswith( "SETS" ): 1159 | #print( buffer ) 1160 | try: 1161 | frame_sets = int(buffer[4:]) 1162 | except Exception as e: 1163 | print( buffer, e ) 1164 | self.errorflag.value += 1 1165 | self.textqueue.put( 'Error: ' + buffer ) 1166 | 1167 | initialize_recordprocessing() 1168 | 1169 | elif buffer.startswith( "EVERY" ): 1170 | #print( buffer ) 1171 | try: 1172 | frame_every = int(buffer[ 5:]) 1173 | except Exception as e: 1174 | print( buffer, e ) 1175 | self.errorflag.value += 1 1176 | self.textqueue.put( 'Error: ' + buffer ) 1177 | 1178 | # --------------------------------------- 1179 | # Triggered 1180 | elif buffer.startswith( "TRIGGERED SINGLES START" ): 1181 | initialize_parameters() 1182 | frame_mode = "TRIGGER" 1183 | 1184 | elif buffer.startswith( "TRIGGERED SETS START" ): 1185 | initialize_parameters() 1186 | frame_mode = "TRIGGERSETS" 1187 | 1188 | initialize_recordprocessing() 1189 | 1190 | # --------------------------------------- 1191 | # Clocked 1192 | elif buffer.startswith( "CLOCKED START" ): 1193 | initialize_parameters() 1194 | frame_mode = "CLOCKED" 1195 | 1196 | initialize_recordprocessing() 1197 | 1198 | elif buffer.startswith( "OUTER CLOCK" ): 1199 | try: 1200 | frame_outerclock = int(buffer[11:]) 1201 | except Exception as e: 1202 | print( buffer, e ) 1203 | self.errorflag.value += 1 1204 | self.textqueue.put( 'Error: ' + buffer ) 1205 | 1206 | 1207 | # --------------------------------------- 1208 | elif buffer.startswith( "GATE START" ): 1209 | #print( buffer ) 1210 | frame_mode = "GATE" 1211 | initialize_recordprocessing() 1212 | 1213 | # --------------------------------------- 1214 | elif buffer.startswith( "ADC" ) or buffer.startswith( "CHIPTEMPERATURE" ): 1215 | if busyflag.value: 1216 | dataqueue_put( buffer ) 1217 | else: 1218 | self.textqueue.put( buffer ) 1219 | if self.monitorqueue: 1220 | self.monitorqueue.put( buffer.strip() + '\n' ) 1221 | 1222 | elif buffer.startswith( "COMPLETE" ): 1223 | 1224 | if accumulate: 1225 | enqueueaccumulators() 1226 | 1227 | # --------------------- 1228 | if debug: 1229 | print( 'lccd clearing busyflag' ) 1230 | busyflag.value = 0 1231 | 1232 | 1233 | elif buffer.startswith( "coefficient" ): 1234 | 1235 | # this also generates self.xdata 1236 | self.parsecoefficients( buffer ) 1237 | print( self.coefficients ) 1238 | print( self.xdata ) 1239 | 1240 | elif buffer.startswith( "Identifier" ): 1241 | 1242 | self.identifier = buffer.split(maxsplit=1) 1243 | 1244 | # -------------------------------- 1245 | # Receive Ascii Formatted Text 1246 | elif ( buffer[0] == '#' ): 1247 | print( buffer[1:] ) 1248 | 1249 | elif ( buffer.startswith( "Error:") ): 1250 | 1251 | #print( buffer ); 1252 | self.errorflag.value += 1 1253 | 1254 | self.textqueue.put( buffer ) 1255 | if self.monitorqueue: 1256 | self.monitorqueue.put( buffer.strip() + '\n' ) 1257 | 1258 | else: 1259 | 1260 | self.textqueue.put( buffer ) 1261 | if self.monitorqueue: 1262 | self.monitorqueue.put( buffer.strip() + '\n' ) 1263 | 1264 | print( "reader exit" ) 1265 | sys.exit() 1266 | 1267 | # -------------------------------------------- 1268 | def checkerrors( self ): 1269 | 1270 | counter = self.errorflag.value 1271 | self.errorflag.value = 0 1272 | 1273 | return counter 1274 | 1275 | def read_all_( self ): 1276 | resp = [] 1277 | while not self.textqueue.empty(): 1278 | resp.append( self.textqueue.get() ) 1279 | return resp 1280 | 1281 | def read_to_done_( self ): 1282 | resp = [] 1283 | while True: 1284 | line = self.textqueue.get() 1285 | if self.debug: 1286 | print( "read_to_done_, got:", line ) 1287 | if line.startswith( "DONE"): 1288 | break 1289 | resp.append( line ) 1290 | return resp 1291 | 1292 | def read( self ): 1293 | 1294 | while self.textqueue.empty() and not input_ready(): 1295 | sleep(.1) 1296 | 1297 | return self.read_to_done_() 1298 | 1299 | def read_nowait( self ): 1300 | return self.read_all_() 1301 | 1302 | def write( self, buffer ): 1303 | self.ser.write( buffer.encode() ) 1304 | 1305 | def writeread( self, line ): 1306 | 1307 | self.write( line + '\n' ) 1308 | 1309 | response = self.read() 1310 | for line in response: 1311 | print( 'response: ', line ) 1312 | 1313 | return response 1314 | 1315 | def clear( self ): 1316 | 1317 | while not self.textqueue.empty(): 1318 | self.textqueue.get() 1319 | 1320 | while not self.dataqueue.empty(): 1321 | self.dataqueue.get() 1322 | 1323 | def close( self, ignored=None ): 1324 | 1325 | self.flag.value = 0 1326 | sleep( 0.1 ) 1327 | #self.ser.reset_input_buffer() 1328 | #self.ser.close() 1329 | 1330 | self.readerthread.terminate() 1331 | self.readerthread.join( ) 1332 | 1333 | if self.monitorthread: 1334 | self.monitorthread.terminate() 1335 | self.monitorthread.join() 1336 | 1337 | if self.GraphicsWindow: 1338 | self.GraphicsWindow.close() 1339 | 1340 | # ============================================================================================= 1341 | def savetofile( self, file, timestamp=None, comments = None, write_ascii=True, framesetindex = None, records = None ): 1342 | 1343 | def formattedwrites( file, keys, vals, exclude=None ): 1344 | for key,val in zip( keys,vals ): 1345 | 1346 | if exclude is not None: 1347 | if key in exclude: 1348 | continue 1349 | 1350 | if type(val) in [ float, int ] : 1351 | file.write( '# ' + key + ' = ' + str(val) + '\n' ) 1352 | 1353 | elif type(val) in [ str ] : 1354 | file.write( '# ' + key + ' = "' + str(val) + '"\n' ) 1355 | 1356 | elif type(val) in [ list ] : 1357 | if not len(val): 1358 | file.write( '# ' + key + ' = []\n' ) 1359 | else: 1360 | values = val 1361 | if type(values[0]) in [ float, int ] : 1362 | file.write( '# ' + key + ' = [ ' + ', '.join( [ str(v) for v in values ] ) + ' ]\n' ) 1363 | elif type(values[0]) in [ str ] : 1364 | file.write( '# ' + key + ' = [ "' + '", "'.join( [ str(v) for v in values ] ) + '" ]\n' ) 1365 | 1366 | # ------------------------- 1367 | 1368 | newfile = False 1369 | 1370 | if timestamp is None: 1371 | timestamp = datetime.now() 1372 | 1373 | if type(file) is str: 1374 | 1375 | if '%' in file: 1376 | try: 1377 | exec( "file="+file, self.__dict__, globals() ) 1378 | print(file) 1379 | except Exception as e: 1380 | print( e ) 1381 | return False 1382 | 1383 | if not file.endswith( self.filesuffix ): 1384 | file += "." + timestamp.strftime('%Y%m%d.%H%M%S.%f') + self.filesuffix 1385 | 1386 | try: 1387 | file = open(file,"x") 1388 | print( 'saveto', file ) 1389 | newfile = True 1390 | except Exception as e: 1391 | print( "open " + file ) 1392 | print( e ) 1393 | return False 1394 | 1395 | # ------------------------------------------------ 1396 | 1397 | file.write( '# LCCDController.py version %s\n'% __version__ ) 1398 | 1399 | file.write( '# ' + timestamp.strftime('%Y-%m-%d %H:%M:%S.%f') + '\n' ) 1400 | 1401 | if self.identifier is not None: 1402 | s = self.identifier 1403 | s = s.replace('\r', '') 1404 | s = s.strip() 1405 | file.write( '# identifier = "' + s + '"\n' ) 1406 | 1407 | # Save all of the numerical and string values in the header portion of the class 1408 | keys, vals = zip(*self.__dict__.items()) 1409 | formattedwrites( file, keys, vals, exclude=['identifier','filesuffix'] ) 1410 | 1411 | if comments is not None: 1412 | if type(comments) not in [list,tuple]: 1413 | comments = [comments] 1414 | 1415 | for c in comments: 1416 | file.write( '# comment = "' + c + '"\n' ) 1417 | 1418 | 1419 | file.write( '# header end\n' ) 1420 | 1421 | if records is None: 1422 | records = [] 1423 | while not self.dataqueue.empty(): 1424 | record = self.dataqueue.get() 1425 | records.append(record ) 1426 | if self.dataqueue.empty(): 1427 | sleep(0.2) 1428 | 1429 | print( 'lccd writing', len(records), 'records' ) 1430 | 1431 | wroterecord = False 1432 | for record in records: 1433 | 1434 | # If starting the next record, start with two blank lines 1435 | if wroterecord: 1436 | file.write( "\n" ) 1437 | file.write( "\n" ) 1438 | wroterecord = False 1439 | 1440 | # ---------------------------- 1441 | if type(record) is str: 1442 | file.write( '# ' + record.strip() + '\n' ) 1443 | 1444 | else: 1445 | ycols, \ 1446 | frame_shutter, frame_interval, frame_clock, \ 1447 | frame_counter, frame_outercounter, \ 1448 | frame_frames, frame_sets, frame_every, \ 1449 | frame_mode, frame_elapsed, \ 1450 | accumulate, timestamp = record 1451 | 1452 | vals = record[1:] 1453 | 1454 | keys = "SHUTTER", "INTERVAL", "CLOCK", \ 1455 | "COUNTER", "OUTERCOUNTER", \ 1456 | "FRAMES", "SETS", "EVERY", \ 1457 | "MODE", "ELAPSED", \ 1458 | "ACCUMULATE", "TIMESTAMP" 1459 | 1460 | formattedwrites( file, keys, vals ) 1461 | 1462 | if write_ascii: 1463 | for n, ycol in enumerate(ycols): 1464 | file.write( "# DATA ASCII %d COL %d\n"%(len(ycol),n) ) 1465 | for y in ycol: 1466 | file.write( '%.8f\n'%(y) ); 1467 | file.write( "# END DATA\n" ) 1468 | else: 1469 | for n, ycol in enumerate(ycols): 1470 | file.write( "# DATA %s %d COL %d\n"%(type(ycol[0]), len(ycol),n) ) 1471 | file.write( "# END DATA\n" ) 1472 | 1473 | wroterecord = True 1474 | 1475 | if newfile: 1476 | file.close() 1477 | 1478 | # ===================================================================== 1479 | def commandlineprocessor( self, line, fileprefix=None ): 1480 | 1481 | # String ubstitutions 1482 | if '%(' in line: 1483 | parts = list( split_bracketed( line ) ) 1484 | for n,p in enumerate(parts): 1485 | if p.startswith('"') and '"%(' in p: 1486 | exec( "res="+p, self.__dict__, globals() ) 1487 | parts[n] =res 1488 | line = ' '.join( parts ) 1489 | print( "command with string substitution:", line ) 1490 | 1491 | if self.monitorqueue: 1492 | self.monitorqueue.put( 'command: ' + line + '\n') 1493 | 1494 | 1495 | 1496 | if line in [ 'h', 'help' ]: 1497 | 1498 | self.write( 'help\n' ) 1499 | response = self.read() 1500 | 1501 | print( " " ) 1502 | 1503 | print( "Commands implemented in the CLI/host computer:" ) 1504 | print( " h|help - produces this help text" ) 1505 | print( "" ) 1506 | print( " accumulator on | off - turn frame-wise accumulator on/off in the reader thread" ) 1507 | print( " baseline on | off - turn basline correction on/off" ) 1508 | print( "" ) 1509 | print( " add init - initialize the local accumulators" ) 1510 | print( " add - get and add the contents of the data queue to the local accumulators" ) 1511 | print( " add push - push the local accumulators onto the dataqueue for the save command" ) 1512 | print( "" ) 1513 | print( " save fileprefix comments... - save data to diskfile" ) 1514 | print( " wait - wait for completion of the active frameset" ) 1515 | print( "" ) 1516 | print( " @filespec - read and execute commands from a file" ) 1517 | print( " in batch files,';' is a command separator, use \; to escape for shell commands" ) 1518 | print( "" ) 1519 | print( " !command - execute shell command" ) 1520 | print( "" ) 1521 | print( " a = 3 - '=' causes evaluation as python" ) 1522 | print( " = python statement - pass to python interprator" ) 1523 | print( " these commands have access to local() and class name spaces" ) 1524 | print( "" ) 1525 | print( " q[uit] - exit the cli program" ) 1526 | 1527 | elif line.startswith( '#' ): 1528 | print( "rcvd comment line" ) 1529 | print( line ) 1530 | 1531 | elif line.startswith('wait'): 1532 | 1533 | pars = line.split() 1534 | if len(pars) > 3: 1535 | try: 1536 | if not self.wait( float(pars[2] ), bool(pars[3]) ): 1537 | return False 1538 | except Exception as e: 1539 | print( e ) 1540 | return False 1541 | elif len(pars) > 2: 1542 | try: 1543 | if not self.wait( float(pars[2]), True ): 1544 | return False 1545 | except Exception as e: 1546 | print( e ) 1547 | return False 1548 | else: 1549 | if not self.wait( interruptible=True ): 1550 | return False 1551 | 1552 | elif line.startswith('clear'): 1553 | self.clear() 1554 | 1555 | elif line.startswith('save'): 1556 | 1557 | pars = line.split( maxsplit = 2 ) 1558 | 1559 | print( pars ) 1560 | 1561 | if len( pars ) == 1: 1562 | print( 'need filespec [comments]' ) 1563 | 1564 | elif len(pars) == 2: 1565 | 1566 | self.savetofile( pars[1], write_ascii=True ) 1567 | 1568 | elif len(pars) == 3: 1569 | 1570 | self.savetofile( pars[1], comments = pars[2], write_ascii=True ) 1571 | 1572 | # Local accumulator in the CLI 1573 | elif line.startswith('add') or line.startswith( "local accumulat" ): 1574 | 1575 | # Initialize the local accumulator 1576 | if 'init' in line: 1577 | return self.accumulators.initialize() 1578 | 1579 | if 'graph' in line: 1580 | return self.accumulators.graph() 1581 | 1582 | # Put it back on the queue 1583 | if 'push' in line: 1584 | return self.accumulators.push() 1585 | 1586 | # Normal acculumulator add 1587 | return self.accumulators.pull() 1588 | 1589 | # Accumulate in the reader thread 1590 | elif line.startswith( 'accumulat' ): 1591 | if 'off' in line: 1592 | self.accumulatorflag.value = 0 1593 | else: 1594 | self.accumulatorflag.value = 1 1595 | 1596 | elif line.startswith( 'baseline' ): 1597 | if 'off' in line: 1598 | self.baselineflag.value = 0 1599 | else: 1600 | self.baselineflag.value = 1 1601 | 1602 | elif line.startswith('@'): 1603 | 1604 | batchfile = line[1:].strip() 1605 | 1606 | try: 1607 | with open( batchfile, 'r' ) as f: 1608 | script = f.readlines() 1609 | except Exception as e: 1610 | print(e) 1611 | return False 1612 | 1613 | for line in script: 1614 | line = line.strip() 1615 | status = True 1616 | 1617 | # Protect the semicolons 1618 | if ';' in line: 1619 | line = line.replace( '\;', '{\semicolon}' ) 1620 | 1621 | for line_ in line.split(';'): 1622 | 1623 | # Restore the semicolons 1624 | line_ = line_.replace( '{\semicolon}', ';' ) 1625 | line_ = line_.strip() 1626 | print( "command:", line_ ) 1627 | 1628 | if not self.commandlineprocessor( line_, fileprefix ): 1629 | status = False 1630 | break 1631 | if not status: 1632 | break 1633 | 1634 | elif line.startswith('!'): 1635 | result = os.popen( line[1:] ).read() 1636 | if result: 1637 | print( result ) 1638 | 1639 | elif '=' in line: 1640 | 1641 | if line == '=': 1642 | for key, val in globals().items(): 1643 | print( "global ", key, val ) 1644 | for key, val in locals().items(): 1645 | print( "local ",key, val ) 1646 | 1647 | elif line.startswith( '=' ): 1648 | try: 1649 | exec( line[1:].strip(), self.__dict__, globals() ) 1650 | except Exception as e: 1651 | print( e ) 1652 | return False 1653 | 1654 | else: 1655 | try: 1656 | exec( line, self.__dict__, globals() ) 1657 | except Exception as e: 1658 | print( e ) 1659 | return False 1660 | 1661 | elif line.startswith('for'): 1662 | 1663 | loopspec, line_ = line.split(':',maxsplit=1) 1664 | 1665 | parts = list( split_bracketed( loopspec ) ) 1666 | if len( parts ) != 4 or parts[2] != 'in': 1667 | print( 'loopspec not valid', loopspec ) 1668 | return False 1669 | 1670 | exec( "loopvalues = list(" + parts[3] + ")", self.__dict__, globals() ) 1671 | print( loopvalues ) 1672 | 1673 | for v in loopvalues: 1674 | 1675 | exec( parts[1] + " = " + str(v), self.__dict__, globals() ) 1676 | 1677 | for line__ in line_.split(';'): 1678 | 1679 | line__ = line__.strip() 1680 | 1681 | if line__.startswith( '"' ): 1682 | exec( "line___ = " + line__, self.__dict__, globals() ) 1683 | print( "command:", line___ ) 1684 | if not self.commandlineprocessor( line___, fileprefix ): 1685 | return False 1686 | else: 1687 | print( "command:", line__ ) 1688 | if not self.commandlineprocessor( line__, fileprefix ): 1689 | return False 1690 | 1691 | 1692 | elif line.startswith( 'parse' ): 1693 | try: 1694 | print( list( split_bracketed( line ) ) ) 1695 | except Exception as e: 1696 | print( e ) 1697 | 1698 | elif line is not None and len(line) > 0: 1699 | 1700 | self.write( line + '\n' ) 1701 | 1702 | response = self.read() 1703 | for line in response: 1704 | print( 'response: ', line ) 1705 | 1706 | else: 1707 | response = self.read_nowait() 1708 | if len(response) : 1709 | for line in response: 1710 | print( '>response: ', line ) 1711 | 1712 | if self.checkerrors(): 1713 | print( 'checkerrors found errors' ) 1714 | return False 1715 | 1716 | return True 1717 | 1718 | 1719 | def commandloop( self, name="SerialMonitor", fileprefix=None ): 1720 | 1721 | while self.flag.value: 1722 | 1723 | line = input( name + ':' ) 1724 | 1725 | if line.lower() in ['exit', 'quit', 'q' ]: 1726 | break 1727 | 1728 | count = self.checkerrors() 1729 | if count: 1730 | print( "rcvd %d errors previous to this command"%(count) ) 1731 | 1732 | self.commandlineprocessor( line, fileprefix ) 1733 | 1734 | count = self.checkerrors() 1735 | if count: 1736 | print( "Error detected", count ) 1737 | 1738 | return 1739 | 1740 | # ========================================================================================================= 1741 | 1742 | if __name__ == "__main__": 1743 | 1744 | import argparse 1745 | 1746 | try: 1747 | import readline 1748 | except: 1749 | pass 1750 | 1751 | import atexit 1752 | import signal 1753 | 1754 | if platform.system() == 'Linux': 1755 | ser0_default = '/dev/ttyACM0' 1756 | ser1_default = '/dev/ttyACM1' 1757 | elif platform.system() == 'Windows': 1758 | ser0_default = 'COM1:' 1759 | ser1_default = 'COM2:' 1760 | 1761 | # --------------------------------------------------------- 1762 | def SignalHandler(signal, frame): 1763 | print('Ctrl-C') 1764 | 1765 | serialdevice.close() 1766 | if dataport is not None: 1767 | dataport.close() 1768 | 1769 | print('Exit') 1770 | sys.exit(0) 1771 | 1772 | # --------------------------------------------------------- 1773 | class ExplicitDefaultsHelpFormatter(argparse.ArgumentDefaultsHelpFormatter): 1774 | def _get_help_string(self, action): 1775 | if action.default in (None, False): 1776 | return action.help 1777 | return super()._get_help_string(action) 1778 | 1779 | parser = argparse.ArgumentParser( description='LCCD Controler Monitor/Cli', 1780 | formatter_class=ExplicitDefaultsHelpFormatter ) 1781 | 1782 | parser.add_argument( 'ports', default=[ser0_default], nargs='*', 1783 | help = 'one or more serial or com ports,' + 1784 | ' the first is the control port, others are readonly' ) 1785 | 1786 | parser.add_argument( '--historyfile', default = 'tcd1304rev2controller.history', help='history file for the command line interface' ) 1787 | parser.add_argument( '--nohistoryfile' ) 1788 | 1789 | parser.add_argument( '--pixels', action = 'store_true', help='graph pixel indices on x-axis' ) 1790 | 1791 | parser.add_argument( '--guiwindow', action = 'store_true', help='gui version of the graphical display' ) 1792 | 1793 | parser.add_argument( '--loggingwindow', action = 'store_true', help='display transactions in a scrolling text window' ) 1794 | 1795 | parser.add_argument( '--raw', action = 'store_true', help='graph raw binary values' ) 1796 | 1797 | parser.add_argument( '--debug', action = 'store_true' ) 1798 | 1799 | # for datafile reading 1800 | parser.add_argument( '--xrange', nargs=2, type=float ) 1801 | parser.add_argument( '--yrange', nargs=2, type=float ) 1802 | parser.add_argument( '--dump', action = 'store_true' ) 1803 | parser.add_argument( '--graph', action = 'store_true' ) 1804 | parser.add_argument( '--frame', type=int ) 1805 | parser.add_argument( '--x', help = 'xdata or python rexpression' ) 1806 | parser.add_argument( '--y', help = 'ydata or python expression' ) 1807 | parser.add_argument( '--output' ) 1808 | 1809 | args = parser.parse_args() 1810 | 1811 | # ---------------------------------------------------------- 1812 | if os.path.isfile( args.ports[0] ): 1813 | print( 'reading file', args.ports[0] ) 1814 | dataobject = LCCDDATA( args.ports[0] ) 1815 | if args.dump: 1816 | dataobject.dump() 1817 | if args.graph: 1818 | xdata = dataobject.xdata 1819 | if args.x is not None: 1820 | exec( 'xdata = '+args.x ) 1821 | if args.frame is not None: 1822 | frame = dataobject.frames[args.frame] 1823 | ydata = frame.data 1824 | if args.y is not None: 1825 | exec( 'ydata = '+args.y ) 1826 | plt.plot( xdata, ydata ) 1827 | else: 1828 | for n, frame in enumerate(dataobject.frames): 1829 | ydata = frame.data 1830 | if args.y is not None: 1831 | exec( 'ydata = '+args.y ) 1832 | plt.plot( xdata, ydata,label=str(frame.offset)) 1833 | plt.legend(title=dataobject.mode+', t(secs)') 1834 | 1835 | if args.output is not None: 1836 | plt.savefig( args.output ) 1837 | else: 1838 | plt.show() 1839 | 1840 | quit() 1841 | 1842 | # --------------------------------------------------------- 1843 | serialdevice = LCCDCONTROLLER( args.ports[0], monitor=args.loggingwindow, graph_by_pixels=args.pixels, gui=args.guiwindow, debug=args.debug ) 1844 | dataport = None 1845 | 1846 | if len(args.ports) > 1: 1847 | dataport = LCCDCONTROLLER( args.ports[1] ) 1848 | 1849 | # --------------------------------------------------------- 1850 | if not args.nohistoryfile: 1851 | try: 1852 | readline.read_history_file(args.historyfile) 1853 | except Exception as e: 1854 | print('historyfile: ', e) 1855 | print('continuing') 1856 | 1857 | atexit.register(readline.write_history_file, args.historyfile) 1858 | 1859 | signal.signal(signal.SIGINT, SignalHandler) 1860 | 1861 | # --------------------------------------------------------- 1862 | 1863 | sleep(1) 1864 | print( "" ) 1865 | print( versionstring ) 1866 | 1867 | serialdevice.commandloop( name="LCCD", fileprefix=None ) 1868 | 1869 | serialdevice.close() 1870 | -------------------------------------------------------------------------------- /TCD1304Rev2_Python/help.txt: -------------------------------------------------------------------------------- 1 | Report device, version and configuration 2 | version - report software version 3 | configuration - report device configuration and data structure 4 | pins - report digital i/o functions and pin numbers 5 | 6 | Stop clocked and interrupt driven reads and the idler 7 | stop - stop clocks, triggers, gate. 8 | stop idle[r] - stop the idler. 9 | stop all - stop all, including the idler 10 | 11 | Coefficients for pixel number to wwavelength 12 | store coefficients 13 | coefficients - report 14 | 15 | Response function coefficients 16 | store response 17 | response - report 18 | 19 | Save/recall dark and response spectra 20 | save dark|resp - save current buffer as dark or response 21 | recall dark|resp - and send it to the host 22 | 23 | upload int|float - followed by values one per line from the host 24 | 25 | save filename - save from current buffer to internal file 26 | recall filename - read from internal file to current buffer and send 27 | 28 | Select current working buffer from the buffer ring 29 | select buffer n - select/report current buffer by number 30 | send - send contents of the current buffer 31 | 32 | Identifier string (63 bytes) 33 | store 34 | identifier - list identifier string 35 | 36 | Data format 37 | set ascii - set data format to ascii 38 | set binary - set data format to binary 39 | format - report data form 40 | 41 | Microcontroller temperature 42 | temperature - report microcontroller temperature 43 | 44 | Read and average analog inputs 45 | adcs - read analog inputs and report 46 | set adcs - read ADCs at frame completion 47 | set adcs off 48 | 49 | Read, manual loop, , in usecs: 50 | clock read one frame 51 | clock read n frames 52 | clock read n framns 53 | 54 | Clock frames or sets of frames, with , , and in usecs: 55 | clock read one frame 56 | clock read n frames 57 | clock read n framns 58 | clock 59 | clock 60 | 61 | Trigger clocked frames: 62 | trigger trigger one frame 63 | trigger trigger n times, single frames 64 | trigger trigger m times, n frames each, frame=shutter 65 | trigger trigger m sets of n frames 66 | 67 | For gate frames (shutter opens and closes on change 68 | gate - gate n frames, default to 1 69 | 70 | set trigger rising 71 | set trigger falling 72 | set trigger change 73 | 74 | set trigger pullup set trigger input to pullup 75 | clear trigger pullup 76 | 77 | Latched frames, send only with increasing maximum 78 | latch 79 | latch 80 | clear latch 81 | 82 | set holdoff usecs 83 | clear holdoff 84 | 85 | Sync pin output at frame start, shutter or holdoff 86 | set sync shutter 87 | set sync start 88 | set sync holdoff [usecs] 89 | set sync off 90 | clear sync 91 | 92 | Pin control, sync and busy are toggled with the shutters 93 | set spare|sync|busy hi 94 | set spare|sync|busy lo 95 | pulse spare|sync|busy [usecs] 96 | toggle spare|sync|busy 97 | 98 | Idler 99 | set idler off - set idler off/auto/time spec 100 | set idler auto 101 | set idler 102 | 103 | Pulse width modulation - spare pin 104 | pwm 105 | pwm off 106 | 107 | Preconfigured pins 108 | Trigger(input)2 Busy 1 Sync 0 Spare 3 109 | 110 | Commands implemented in the CLI/host computer: 111 | h|help - produces this help text 112 | 113 | accumulator on | off - turn frame-wise accumulator on/off in the reader thread 114 | baseline on | off - turn basline correction on/off 115 | 116 | add init - initialize the local accumulators 117 | add - get and add the contents of the data queue to the local accumulators 118 | add push - push the local accumulators onto the dataqueue for the save command 119 | 120 | save fileprefix comments... - save data to diskfile 121 | wait - wait for completion of the active frameset 122 | 123 | @filespec - read and execute commands from a file 124 | in batch files,';' is a command separator, use \; to escape for shell commands 125 | 126 | !command - execute shell command 127 | 128 | a = 3 - '=' causes evaluation as python 129 | = python statement - pass to python interprator 130 | these commands have access to local() and class name spaces 131 | 132 | q[uit] - exit the cli program 133 | 134 | -------------------------------------------------------------------------------- /TCD1304Rev2_Python/tcd1304rev2controller.history: -------------------------------------------------------------------------------- 1 | version 2 | read 10000 3 | read 100000 4 | clock 1000 100000 5 | stop 6 | q 7 | help 8 | q 9 | clock 10 100000 10 | q 11 | -------------------------------------------------------------------------------- /TCD1304Rev2b.asc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/a3f1854eeb2a54a471a1fbc36f44e99b621c3136/TCD1304Rev2b.asc -------------------------------------------------------------------------------- /TCD1304Rev2c_KiCAD/TCD13042c.kicad_pro: -------------------------------------------------------------------------------- 1 | { 2 | "board": { 3 | "3dviewports": [], 4 | "design_settings": { 5 | "defaults": { 6 | "apply_defaults_to_fp_fields": false, 7 | "apply_defaults_to_fp_shapes": false, 8 | "apply_defaults_to_fp_text": false, 9 | "board_outline_line_width": 0.1, 10 | "copper_line_width": 0.2, 11 | "copper_text_italic": false, 12 | "copper_text_size_h": 1.5, 13 | "copper_text_size_v": 1.5, 14 | "copper_text_thickness": 0.3, 15 | "copper_text_upright": false, 16 | "courtyard_line_width": 0.05, 17 | "dimension_precision": 4, 18 | "dimension_units": 3, 19 | "dimensions": { 20 | "arrow_length": 1270000, 21 | "extension_offset": 500000, 22 | "keep_text_aligned": true, 23 | "suppress_zeroes": false, 24 | "text_position": 0, 25 | "units_format": 1 26 | }, 27 | "fab_line_width": 0.1, 28 | "fab_text_italic": false, 29 | "fab_text_size_h": 1.0, 30 | "fab_text_size_v": 1.0, 31 | "fab_text_thickness": 0.15, 32 | "fab_text_upright": false, 33 | "other_line_width": 0.15, 34 | "other_text_italic": false, 35 | "other_text_size_h": 1.0, 36 | "other_text_size_v": 1.0, 37 | "other_text_thickness": 0.15, 38 | "other_text_upright": false, 39 | "pads": { 40 | "drill": 0.0, 41 | "height": 5.6, 42 | "width": 5.6 43 | }, 44 | "silk_line_width": 0.15, 45 | "silk_text_italic": false, 46 | "silk_text_size_h": 1.0, 47 | "silk_text_size_v": 1.0, 48 | "silk_text_thickness": 0.15, 49 | "silk_text_upright": false, 50 | "zones": { 51 | "45_degree_only": false, 52 | "min_clearance": 0.508 53 | } 54 | }, 55 | "diff_pair_dimensions": [ 56 | { 57 | "gap": 0.0, 58 | "via_gap": 0.0, 59 | "width": 0.0 60 | } 61 | ], 62 | "drc_exclusions": [ 63 | "footprint_symbol_mismatch|150000000|100000000|b1da0ca6-6d5b-45ca-9bac-e49f657b7175|00000000-0000-0000-0000-000000000000", 64 | "lib_footprint_mismatch|160795000|100000000|51d77513-7fd1-45ca-be99-6d224c6d2abb|00000000-0000-0000-0000-000000000000", 65 | "silk_over_copper|142380000|104435000|fd83f44e-a5ec-4e10-9b5b-66d91b828432|00000000-0000-0000-0000-000000000000", 66 | "text_height|159906000|100000000|0dfbbe8d-bc94-4096-9efa-38c40325018e|00000000-0000-0000-0000-000000000000", 67 | "via_dangling|131365000|101140000|78b54389-d786-4e39-8091-236d99e61fed|00000000-0000-0000-0000-000000000000" 68 | ], 69 | "meta": { 70 | "version": 2 71 | }, 72 | "rule_severities": { 73 | "annular_width": "error", 74 | "clearance": "error", 75 | "connection_width": "warning", 76 | "copper_edge_clearance": "error", 77 | "copper_sliver": "warning", 78 | "courtyards_overlap": "error", 79 | "diff_pair_gap_out_of_range": "error", 80 | "diff_pair_uncoupled_length_too_long": "error", 81 | "drill_out_of_range": "error", 82 | "duplicate_footprints": "warning", 83 | "extra_footprint": "warning", 84 | "footprint": "error", 85 | "footprint_symbol_mismatch": "warning", 86 | "footprint_type_mismatch": "error", 87 | "hole_clearance": "error", 88 | "hole_near_hole": "error", 89 | "holes_co_located": "warning", 90 | "invalid_outline": "error", 91 | "isolated_copper": "warning", 92 | "item_on_disabled_layer": "error", 93 | "items_not_allowed": "error", 94 | "length_out_of_range": "error", 95 | "lib_footprint_issues": "warning", 96 | "lib_footprint_mismatch": "warning", 97 | "malformed_courtyard": "error", 98 | "microvia_drill_out_of_range": "error", 99 | "missing_courtyard": "ignore", 100 | "missing_footprint": "warning", 101 | "net_conflict": "warning", 102 | "npth_inside_courtyard": "ignore", 103 | "padstack": "error", 104 | "pth_inside_courtyard": "ignore", 105 | "shorting_items": "error", 106 | "silk_edge_clearance": "warning", 107 | "silk_over_copper": "warning", 108 | "silk_overlap": "warning", 109 | "skew_out_of_range": "error", 110 | "solder_mask_bridge": "error", 111 | "starved_thermal": "error", 112 | "text_height": "warning", 113 | "text_thickness": "warning", 114 | "through_hole_pad_without_hole": "error", 115 | "too_many_vias": "error", 116 | "track_dangling": "warning", 117 | "track_width": "error", 118 | "tracks_crossing": "error", 119 | "unconnected_items": "error", 120 | "unresolved_variable": "error", 121 | "via_dangling": "warning", 122 | "zone_has_empty_net": "error", 123 | "zones_intersect": "error" 124 | }, 125 | "rules": { 126 | "allow_blind_buried_vias": false, 127 | "allow_microvias": false, 128 | "max_error": 0.005, 129 | "min_clearance": 0.0, 130 | "min_connection": 0.0, 131 | "min_copper_edge_clearance": 0.0, 132 | "min_hole_clearance": 0.25, 133 | "min_hole_to_hole": 0.25, 134 | "min_microvia_diameter": 0.2, 135 | "min_microvia_drill": 0.1, 136 | "min_resolved_spokes": 2, 137 | "min_silk_clearance": 0.0, 138 | "min_text_height": 0.8, 139 | "min_text_thickness": 0.08, 140 | "min_through_hole_diameter": 0.3, 141 | "min_track_width": 0.2, 142 | "min_via_annular_width": 0.05, 143 | "min_via_diameter": 0.4, 144 | "solder_mask_clearance": 0.0, 145 | "solder_mask_min_width": 0.0, 146 | "solder_mask_to_copper_clearance": 0.0, 147 | "use_height_for_length_calcs": true 148 | }, 149 | "teardrop_options": [ 150 | { 151 | "td_onpadsmd": true, 152 | "td_onroundshapesonly": false, 153 | "td_ontrackend": false, 154 | "td_onviapad": true 155 | } 156 | ], 157 | "teardrop_parameters": [ 158 | { 159 | "td_allow_use_two_tracks": true, 160 | "td_curve_segcount": 0, 161 | "td_height_ratio": 1.0, 162 | "td_length_ratio": 0.5, 163 | "td_maxheight": 2.0, 164 | "td_maxlen": 1.0, 165 | "td_on_pad_in_zone": false, 166 | "td_target_name": "td_round_shape", 167 | "td_width_to_size_filter_ratio": 0.9 168 | }, 169 | { 170 | "td_allow_use_two_tracks": true, 171 | "td_curve_segcount": 0, 172 | "td_height_ratio": 1.0, 173 | "td_length_ratio": 0.5, 174 | "td_maxheight": 2.0, 175 | "td_maxlen": 1.0, 176 | "td_on_pad_in_zone": false, 177 | "td_target_name": "td_rect_shape", 178 | "td_width_to_size_filter_ratio": 0.9 179 | }, 180 | { 181 | "td_allow_use_two_tracks": true, 182 | "td_curve_segcount": 0, 183 | "td_height_ratio": 1.0, 184 | "td_length_ratio": 0.5, 185 | "td_maxheight": 2.0, 186 | "td_maxlen": 1.0, 187 | "td_on_pad_in_zone": false, 188 | "td_target_name": "td_track_end", 189 | "td_width_to_size_filter_ratio": 0.9 190 | } 191 | ], 192 | "track_widths": [ 193 | 0.0 194 | ], 195 | "tuning_pattern_settings": { 196 | "diff_pair_defaults": { 197 | "corner_radius_percentage": 80, 198 | "corner_style": 1, 199 | "max_amplitude": 1.0, 200 | "min_amplitude": 0.2, 201 | "single_sided": false, 202 | "spacing": 1.0 203 | }, 204 | "diff_pair_skew_defaults": { 205 | "corner_radius_percentage": 80, 206 | "corner_style": 1, 207 | "max_amplitude": 1.0, 208 | "min_amplitude": 0.2, 209 | "single_sided": false, 210 | "spacing": 0.6 211 | }, 212 | "single_track_defaults": { 213 | "corner_radius_percentage": 80, 214 | "corner_style": 1, 215 | "max_amplitude": 1.0, 216 | "min_amplitude": 0.2, 217 | "single_sided": false, 218 | "spacing": 0.6 219 | } 220 | }, 221 | "via_dimensions": [ 222 | { 223 | "diameter": 0.0, 224 | "drill": 0.0 225 | } 226 | ], 227 | "zones_allow_external_fillets": false, 228 | "zones_use_no_outline": true 229 | }, 230 | "ipc2581": { 231 | "dist": "", 232 | "distpn": "", 233 | "internal_id": "", 234 | "mfg": "", 235 | "mpn": "" 236 | }, 237 | "layer_presets": [], 238 | "viewports": [] 239 | }, 240 | "boards": [], 241 | "cvpcb": { 242 | "equivalence_files": [] 243 | }, 244 | "erc": { 245 | "erc_exclusions": [], 246 | "meta": { 247 | "version": 0 248 | }, 249 | "pin_map": [ 250 | [ 251 | 0, 252 | 0, 253 | 0, 254 | 0, 255 | 0, 256 | 0, 257 | 1, 258 | 0, 259 | 0, 260 | 0, 261 | 0, 262 | 2 263 | ], 264 | [ 265 | 0, 266 | 2, 267 | 0, 268 | 1, 269 | 0, 270 | 0, 271 | 1, 272 | 0, 273 | 2, 274 | 2, 275 | 2, 276 | 2 277 | ], 278 | [ 279 | 0, 280 | 0, 281 | 0, 282 | 0, 283 | 0, 284 | 0, 285 | 1, 286 | 0, 287 | 1, 288 | 0, 289 | 1, 290 | 2 291 | ], 292 | [ 293 | 0, 294 | 1, 295 | 0, 296 | 0, 297 | 0, 298 | 0, 299 | 1, 300 | 1, 301 | 2, 302 | 1, 303 | 1, 304 | 2 305 | ], 306 | [ 307 | 0, 308 | 0, 309 | 0, 310 | 0, 311 | 0, 312 | 0, 313 | 1, 314 | 0, 315 | 0, 316 | 0, 317 | 0, 318 | 2 319 | ], 320 | [ 321 | 0, 322 | 0, 323 | 0, 324 | 0, 325 | 0, 326 | 0, 327 | 0, 328 | 0, 329 | 0, 330 | 0, 331 | 0, 332 | 2 333 | ], 334 | [ 335 | 1, 336 | 1, 337 | 1, 338 | 1, 339 | 1, 340 | 0, 341 | 1, 342 | 1, 343 | 1, 344 | 1, 345 | 1, 346 | 2 347 | ], 348 | [ 349 | 0, 350 | 0, 351 | 0, 352 | 1, 353 | 0, 354 | 0, 355 | 1, 356 | 0, 357 | 0, 358 | 0, 359 | 0, 360 | 2 361 | ], 362 | [ 363 | 0, 364 | 2, 365 | 1, 366 | 2, 367 | 0, 368 | 0, 369 | 1, 370 | 0, 371 | 2, 372 | 2, 373 | 2, 374 | 2 375 | ], 376 | [ 377 | 0, 378 | 2, 379 | 0, 380 | 1, 381 | 0, 382 | 0, 383 | 1, 384 | 0, 385 | 2, 386 | 0, 387 | 0, 388 | 2 389 | ], 390 | [ 391 | 0, 392 | 2, 393 | 1, 394 | 1, 395 | 0, 396 | 0, 397 | 1, 398 | 0, 399 | 2, 400 | 0, 401 | 0, 402 | 2 403 | ], 404 | [ 405 | 2, 406 | 2, 407 | 2, 408 | 2, 409 | 2, 410 | 2, 411 | 2, 412 | 2, 413 | 2, 414 | 2, 415 | 2, 416 | 2 417 | ] 418 | ], 419 | "rule_severities": { 420 | "bus_definition_conflict": "error", 421 | "bus_entry_needed": "error", 422 | "bus_label_syntax": "error", 423 | "bus_to_bus_conflict": "error", 424 | "bus_to_net_conflict": "error", 425 | "conflicting_netclasses": "error", 426 | "different_unit_footprint": "error", 427 | "different_unit_net": "error", 428 | "duplicate_reference": "error", 429 | "duplicate_sheet_names": "error", 430 | "endpoint_off_grid": "warning", 431 | "extra_units": "error", 432 | "global_label_dangling": "warning", 433 | "hier_label_mismatch": "error", 434 | "label_dangling": "error", 435 | "lib_symbol_issues": "warning", 436 | "missing_bidi_pin": "warning", 437 | "missing_input_pin": "warning", 438 | "missing_power_pin": "error", 439 | "missing_unit": "warning", 440 | "multiple_net_names": "warning", 441 | "net_not_bus_member": "warning", 442 | "no_connect_connected": "warning", 443 | "no_connect_dangling": "warning", 444 | "pin_not_connected": "error", 445 | "pin_not_driven": "error", 446 | "pin_to_pin": "warning", 447 | "power_pin_not_driven": "error", 448 | "similar_labels": "warning", 449 | "simulation_model_issue": "ignore", 450 | "unannotated": "error", 451 | "unit_value_mismatch": "error", 452 | "unresolved_variable": "error", 453 | "wire_dangling": "error" 454 | } 455 | }, 456 | "libraries": { 457 | "pinned_footprint_libs": [], 458 | "pinned_symbol_libs": [] 459 | }, 460 | "meta": { 461 | "filename": "TCD1304_All-in-One.kicad_pro", 462 | "version": 1 463 | }, 464 | "net_settings": { 465 | "classes": [ 466 | { 467 | "bus_width": 12, 468 | "clearance": 0.2, 469 | "diff_pair_gap": 0.25, 470 | "diff_pair_via_gap": 0.25, 471 | "diff_pair_width": 0.2, 472 | "line_style": 0, 473 | "microvia_diameter": 0.3, 474 | "microvia_drill": 0.1, 475 | "name": "Default", 476 | "pcb_color": "rgba(0, 0, 0, 0.000)", 477 | "schematic_color": "rgba(0, 0, 0, 0.000)", 478 | "track_width": 0.25, 479 | "via_diameter": 0.8, 480 | "via_drill": 0.4, 481 | "wire_width": 6 482 | }, 483 | { 484 | "bus_width": 12, 485 | "clearance": 0.2, 486 | "diff_pair_gap": 0.25, 487 | "diff_pair_via_gap": 0.25, 488 | "diff_pair_width": 0.2, 489 | "line_style": 0, 490 | "microvia_diameter": 0.3, 491 | "microvia_drill": 0.1, 492 | "name": "Power", 493 | "pcb_color": "rgba(0, 0, 0, 0.000)", 494 | "schematic_color": "rgba(0, 0, 0, 0.000)", 495 | "track_width": 0.35, 496 | "via_diameter": 1.0, 497 | "via_drill": 0.5, 498 | "wire_width": 6 499 | } 500 | ], 501 | "meta": { 502 | "version": 3 503 | }, 504 | "net_colors": null, 505 | "netclass_assignments": null, 506 | "netclass_patterns": [ 507 | { 508 | "netclass": "Power", 509 | "pattern": "+3.3V" 510 | }, 511 | { 512 | "netclass": "Power", 513 | "pattern": "Earth" 514 | } 515 | ] 516 | }, 517 | "pcbnew": { 518 | "last_paths": { 519 | "gencad": "", 520 | "idf": "", 521 | "netlist": "../../../../", 522 | "plot": "TCD1304_All-in-One_FAB", 523 | "pos_files": "TCD1304_All-in-One_FAB", 524 | "specctra_dsn": "", 525 | "step": "TCD1304Rev2.step", 526 | "svg": "", 527 | "vrml": "TCD1304Rev2.wrl" 528 | }, 529 | "page_layout_descr_file": "" 530 | }, 531 | "schematic": { 532 | "annotate_start_num": 0, 533 | "bom_export_filename": "TCD1304_All-in-One_BOM.csv", 534 | "bom_fmt_presets": [], 535 | "bom_fmt_settings": { 536 | "field_delimiter": ",", 537 | "keep_line_breaks": false, 538 | "keep_tabs": false, 539 | "name": "CSV", 540 | "ref_delimiter": ",", 541 | "ref_range_delimiter": "", 542 | "string_delimiter": "\"" 543 | }, 544 | "bom_presets": [], 545 | "bom_settings": { 546 | "exclude_dnp": false, 547 | "fields_ordered": [ 548 | { 549 | "group_by": false, 550 | "label": "Reference", 551 | "name": "Reference", 552 | "show": true 553 | }, 554 | { 555 | "group_by": true, 556 | "label": "Value", 557 | "name": "Value", 558 | "show": true 559 | }, 560 | { 561 | "group_by": false, 562 | "label": "Datasheet", 563 | "name": "Datasheet", 564 | "show": false 565 | }, 566 | { 567 | "group_by": false, 568 | "label": "Footprint", 569 | "name": "Footprint", 570 | "show": true 571 | }, 572 | { 573 | "group_by": false, 574 | "label": "Qty", 575 | "name": "${QUANTITY}", 576 | "show": true 577 | }, 578 | { 579 | "group_by": true, 580 | "label": "DNP", 581 | "name": "${DNP}", 582 | "show": true 583 | }, 584 | { 585 | "group_by": false, 586 | "label": "#", 587 | "name": "${ITEM_NUMBER}", 588 | "show": false 589 | }, 590 | { 591 | "group_by": false, 592 | "label": "Description", 593 | "name": "Description", 594 | "show": true 595 | }, 596 | { 597 | "group_by": false, 598 | "label": "Digikey", 599 | "name": "Digikey", 600 | "show": true 601 | }, 602 | { 603 | "group_by": false, 604 | "label": "MANUFACTURER", 605 | "name": "MANUFACTURER", 606 | "show": false 607 | }, 608 | { 609 | "group_by": false, 610 | "label": "Partnu", 611 | "name": "Partnu", 612 | "show": false 613 | } 614 | ], 615 | "filter_string": "", 616 | "group_symbols": true, 617 | "name": "", 618 | "sort_asc": true, 619 | "sort_field": "Reference" 620 | }, 621 | "connection_grid_size": 50.0, 622 | "drawing": { 623 | "dashed_lines_dash_length_ratio": 12.0, 624 | "dashed_lines_gap_length_ratio": 3.0, 625 | "default_line_thickness": 6.0, 626 | "default_text_size": 50.0, 627 | "field_names": [], 628 | "intersheets_ref_own_page": false, 629 | "intersheets_ref_prefix": "", 630 | "intersheets_ref_short": false, 631 | "intersheets_ref_show": false, 632 | "intersheets_ref_suffix": "", 633 | "junction_size_choice": 3, 634 | "label_size_ratio": 0.375, 635 | "operating_point_overlay_i_precision": 3, 636 | "operating_point_overlay_i_range": "~A", 637 | "operating_point_overlay_v_precision": 3, 638 | "operating_point_overlay_v_range": "~V", 639 | "overbar_offset_ratio": 1.23, 640 | "pin_symbol_size": 25.0, 641 | "text_offset_ratio": 0.15 642 | }, 643 | "legacy_lib_dir": "", 644 | "legacy_lib_list": [], 645 | "meta": { 646 | "version": 1 647 | }, 648 | "net_format_name": "", 649 | "ngspice": { 650 | "fix_include_paths": true, 651 | "fix_passive_vals": false, 652 | "meta": { 653 | "version": 0 654 | }, 655 | "model_mode": 0, 656 | "workbook_filename": "" 657 | }, 658 | "page_layout_descr_file": "", 659 | "plot_directory": "", 660 | "spice_adjust_passive_values": false, 661 | "spice_current_sheet_as_root": false, 662 | "spice_external_command": "spice \"%I\"", 663 | "spice_model_current_sheet_as_root": true, 664 | "spice_save_all_currents": false, 665 | "spice_save_all_dissipations": false, 666 | "spice_save_all_voltages": false, 667 | "subpart_first_id": 65, 668 | "subpart_id_separator": 0 669 | }, 670 | "sheets": [ 671 | [ 672 | "ed66a1bc-23d1-46cc-9d54-9e49f67ae911", 673 | "Root" 674 | ] 675 | ], 676 | "text_variables": {} 677 | } 678 | -------------------------------------------------------------------------------- /TCD1304Rev2c_KiCAD/TCD13042c.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/a3f1854eeb2a54a471a1fbc36f44e99b621c3136/TCD1304Rev2c_KiCAD/TCD13042c.pdf -------------------------------------------------------------------------------- /TCD1304Rev2c_KiCAD/TCD13042c_BOM.xls: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/drmcnelson/Linear-CCD-with-LTSpice-KiCAD-Firmware-and-Python-Library/a3f1854eeb2a54a471a1fbc36f44e99b621c3136/TCD1304Rev2c_KiCAD/TCD13042c_BOM.xls --------------------------------------------------------------------------------