Table of Contents
Une nouvelle maison pour Arietty
L'aqua-terrarium
Les modèles du commerce semblent assez petits … La cible serait 80-100cm x 60cm x 12-15cm d'eau.
La filtration
Pompe / filtre
Voir si il faut ensemencer l'e900 …
Stérilisateur
Construit sur la base d'un tube G4T5 à UV-C de 4W :
sterilisateur.svg
Le chauffage
On veut éviter les plongeurs, l'idée serait un câble chauffant (http://www.ebay.com/itm/310246489792) régulé, tension max 24V, 12V préféré (alim PC).
Puissance
???
S est la surface d'échange eau-air, le 10 est une constante de conduction en .
On prendra le pire possible, Soit 10°C (15°C ambiants pour 25°C dans l'eau).
Hauteur d'eau | Toutes faces exposées | Moitiée des faces en bois | ||
---|---|---|---|---|
Surface (m²) | Power (W) | Surface (m²) | Power (W) | |
12cm | 1.3m2 | 130W | 0.65m2 | 65W |
15cm | 1.38m2 | 138W | 0.69m2 | 69W |
On essayera d'atteindre 180W, soit 15A sous 12V éventuellement en 2 brins si l'alim ATX ne peut tout prendre sur un seul rail, ou alors il faudrait une alim 12V / 240W cheap …
Il faudrait donc 2 brins de 1.6Ω en parallele.
Le fil fait 0.127Ω/pied, soit 0.41666Ω/m, il faut donc des brins de 3.84m, disons 4m (commande de 8m soit 26.25 pieds soit 3 lots pour un cout de 18USD).
Reçu 9.7m de fil (pour 9.15m), 4.2Ω au total, 2.1Ω par brin ⇒ 137W@12V si on met toute la longueur.
Longueur de brin | R brin (Ω) | R total (Ω) | Puissance @ 12V (W) | Courant @ 12V (A) | Ok alim ATX 18A | MOS power (max) (W) |
---|---|---|---|---|---|---|
3m | 1.25 | 0.625 | 230 | 19.2 | X | 6.5 |
3.5m | 1.46 | 0.73 | 197 | 16.5 | V | 4.8 |
4m | 1.66 | 0.83 | 173 | 14.4 | V | 3.6 |
4.5m | 1.875 | 0.937 | 154 | 12.8 | V | 2.9 |
4.85m (tout) | 2.02 | 1.01 | 142 | 11.9 | V | 2.5 |
Régulation
On utilise la moyenne de 2 sondes à base de TMP36 au fond du bac entre 2 passages de câble chauffant.
Un ATMega8L s'occupe de la régulation proportionnelle-intégrale et commande le câble en PWM à environ 30kHz.
Un écran et une paire de boutons servent à contrôler la consigne.
Un troisième TMP36 est utilisé pour récupérer la température ambiante.
Le contrôleur transmet un relevé 1-2 fois par minute par ASK-OOK@433MHz (utilisation d'un module standard) sur la base du protocole SerialDevice.
Format de transmission sans fil
Base : 1 start, 1 stop, parité paire, 4800 bauds.
Les frames ont la forme suivante :
- 1 start byte (STX = 0x02)
- 1 sender address byte
- 16 data bytes
- 1 CRC (Fletcher 8-bit) byte
- 1 stop byte (ETX = 0x03)
L'émission est OBLIGATOIREMENT faite à intervalles IRREGULIERS pour éviter l'overlap si plusieurs senders (implémenter un générateur pseudo-random 16bits à XOR).
Une frame fait 20 octets, soit 220 “états”, il faut 46ms pour l’émettre.
Firmware
- main.c
/** * @project Arietty temperature controller * @part main * @autor MELEARD Etienne * @date 2012 * @device ATMega8L * @clock 8MHz (internal RC oscillator) * @avrdude -U lfuse:w:0xc4:m -U hfuse:w:0xd1:m * * Dual probe heating wire aquarium temperature controller with PI regulator and wireless data link reporting. * **/ /**@macros**/ #include <avr/io.h> #include <sup_avr/io2.h> #include <avr/interrupt.h> #include <util/delay.h> /**@iodef**/ // Buttons #define BTN_PLUS Bit(PORTD).bit2 #define SIG_PLUS SIG_INTERRUPT0 #define BTN_MINUS Bit(PORTD).bit3 #define SIG_MINUS SIG_INTERRUPT1 // LCD #define LCD_RS Bit(PORTC).bit0 #define LCD_EN Bit(PORTC).bit1 #define LCD_D0 Bit(PORTC).bit2 #define LCD_D1 Bit(PORTC).bit3 #define LCD_D2 Bit(PORTC).bit4 #define LCD_D3 Bit(PORTD).bit0 // Probes #define PROBE_AMB 5 // ADC5 #define PROBE_0 6 // ADC6 #define PROBE_1 7 // ADC7 // Heater driver #define PWM_OUT Bit(PORTB).bit1 // Debug led #define LED Bit(PIND).bit4 /**@constants**/ // Temperature limits #define MIN_TEMP 24 #define MAX_TEMP 32 // PI regulator parameters #define PI_K 0.5 // 1/[°C] => [W]/[°C] => full power if 2°C error #define PI_I 0.05 // 1/([°C][t_unit]) => full power if 1°C error for 20 minutes (1/20) /**@globals**/ // Temperatures typedef struct { // In °C float target, probe_0, probe_1, tank, ambient; } Ttemperatures; volatile Ttemperatures temperatures = {.target = 0.0, .probe_0 = 0.0, .probe_1 = 0.0, .tank = 0.0, .ambient = 0.0}; // Lcd typedef struct { uint8_t target, probe_0, probe_1, tank, power, ambient; } TintValues; volatile TintValues int_values = {.target = 0, .probe_0 = 0, .probe_1 = 0, .tank = 0, .power = 0, .ambient = 0}; #define LCD_NORMAL_MODE_NORMAL_SCREEN_LINE 0 #define LCD_NORMAL_MODE_ADVANCED_SCREEN_LINE 1 #define LCD_SET_MODE_LINE 1 #define LCD_NORMAL_MODE_NORMAL_SCREEN_TANK_INDEX 12 #define LCD_NORMAL_MODE_NORMAL_SCREEN_TARGET_INDEX 2 #define LCD_NORMAL_MODE_NORMAL_SCREEN_POWER_INDEX 12 #define LCD_NORMAL_MODE_ADVANCED_SCREEN_PROBE_0_INDEX 3 #define LCD_NORMAL_MODE_ADVANCED_SCREEN_PROBE_1_INDEX 13 #define LCD_NORMAL_MODE_ADVANCED_SCREEN_AMBIENT_INDEX 8 #define LCD_SET_MODE_TARGET_INDEX 11 volatile const char lcd_strings[3][2][16] = { {"Temperature XX°C", "C=XX°C P=XXX%"}, {"P0=XX°C P1=XX°C", "Ambient XX°C "}, {"Temperature to ", "maintain : XX°C"} }; // wireless data link message, MUST be 16 byte long #define SERIALDEVICE_ADDRESS 0x01 volatile uint8_t message[16] = { 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00 }; // Random number generator parameters typedef struct { uint8_t a, b, c, x; } TrandomData; volatile TrandomData random_data = {.a = 0, .b = 0, .c = 0, .x = 0}; // UI typedef struct { uint8_t value, counter, previous; } Tstate; #define BTN_CHANGE_MODE_PUSH_DURATION 8 volatile Tstate btn = {.value = 0, .counter = 0, previous = 0}; #define NORMAL_MODE 0 #define SET_MODE 1 #define MODE_IDLE_DURATION 20 volatile Tstate mode = {.value = NORMAL_MODE, .counter = 0, previous = NORMAL_MODE}; #define NORMAL_SCREEN 0 #define ADVANCED_SCREEN 1 #define SCREEN_IDLE_DURATION 20 volatile Tstate screen = {.value = NORMAL_SCREEN, .counter = 0, previous = ADVANCED_SCREEN}; // At startup there must be a difference between value and previous somewhere to force a screen refresh. /**@functions**/ /**@function-category I/O and peripherals init **/ void init(void) { cli(); // Inputs //DDRD &= 0xf3; // Buttons as inputs PORTD = 0x0c; // Pull-up on PD3/INT1 and PD2/INT0 // Outputs DDRB |= 0x02; // PWM output DDRC |= 0x1f; // LCD control DDRD |= 0x11; // LCD control + Led // Button Irq MCUCR |= 0x05; // Irq on all edges GICR |= 0xc0; // on both INT0 and INT1 // Timer 1 used for PWM TCCR1A = 0x81; // Fast PWM, 8bit TCCR1B = 0x40; // 8 bit, prescaler = 1 => 31.25kHz OCR1A = 0x0000; // Power level // ADC // Vref given from 5V through 12k / 47k divider => 1.017V // 1 bit is equal to 0.993mV so 1 degree is roughly 10bit // Exact formulae : T = <m>{1 / 0.01} * (n * {{5 * 12000} / {1023 * (12000 + 47000)}} - 0.5)</m> // T = 0.0994 * n - 50 ADCSRA = 0x87; // Manual mode, 128 prescaler (0.2ms/conv) ADMUX = 0x00; // Right adjust, external ref // USART UCSRA = 0x00; // Normal speed UCSRB = 0x08; // Tx enable, 8 bit data UCSRC = 0xa6; // URSEL, Async mode, even parity, 1 stop bit, 8 bit data UBRR = 103; // 4800 bds @ 8MHz (0.2% error, musn't transmit more than 50 bytes at once) sei(); } /**@function-category EEPROM functions **/ // Read byte from EEPROM uint8_t eepromReadByte(uint16_t addr) { while(EECR & (1<<EEWE)); // Wait for availability EEAR = addr; // Address EECR |= 1<<EERE; // Read enable return EEDR; // Data } // Write byte into EEPROM void eepromWriteByte(uint16_t addr, uint8_t v) { while(EECR & (1<<EEWE)); // Wait for availability EEAR = addr; // Address EEDR = v; // Data EECR |= 1<<EEMWE; // Master write enable EECR |= 1<<EEWE; // Write enable } /**@function-category Random number generator **/ // Courtesy of http://www.electro-tech-online.com/general-electronics-chat/124249-ultra-fast-pseudorandom-number-generator-8-bit.html // Get a random byte uint8_t random(void) { random_data.x++; random_data.a = (random_data.a ^ random_data.c ^ random_data.x); random_data.b = (random_data.b + random_data.a); random_data.c = (random_data.c + (random_data.b>>1) ^ random_data.a); return random_data.c; } // Seed the generator void randomSeed(uint8_t s1, uint8_t s2, uint8_t s3) { random_data.a ^= s1; random_data.b ^= s2; random_data.c ^= s3; random(); } /**@function-category Wireless data link **/ // Send a single byte void serialDeviceSendByte(uint8_t data) { while(!(UCSRA & (1<<UDRE))); UDR = data; } // Send the whole message (must be 16 byte long) void serialDeviceSendMessage(uint8_t *message) { uint16_t sum1 = 0x000f, sum2 = 0x000f; uint8_t len = 16; if(!SERIALDEVICE_ADDRESS) return; serialDeviceSendByte(0x02); // STX serialDeviceSendByte(SERIALDEVICE_ADDRESS); // Sender address sum2 += sum1 += SERIALDEVICE_ADDRESS; // Merge into checksum // Send data while computing 8-bit fletcher checksum while(len--) { serialDeviceSendByte(*message); sum2 += sum1 += *message; message++; } while(pad--) serialDeviceSendByte(0x00); // Padding sum1 = (sum1 & 0x0f) + (sum1 >> 4); sum1 = (sum1 & 0x0f) + (sum1 >> 4); sum2 = (sum2 & 0x0f) + (sum2 >> 4); sum2 = (sum2 & 0x0f) + (sum2 >> 4); serialDeviceSendByte(sum2<<4 | sum1); // Send checksum serialDeviceSendByte(0x03); // ETX } /**@function-category Sensors interface **/ float getTempForProbe(uint8_t addr) { ADMUX = addr & 0x07; // Probe channel selection ADCSRA |= 1<<ADSC; // Start conversion while(ADCSRA & (1<<ADSC)); // Wait for conversion to end return 0.0994 * ADC - 50; // °C } /**@function-category LCD interface **/ // Send half of a command (4 bits) void lcdHalfCmd(uint8_t c) { LCD_D0 = c & 0x01; // Data LCD_D1 = c & 0x02; LCD_D2 = c & 0x04; LCD_D3 = c & 0x08; LCD_EN = 1; // En cycle _delay_us(1); LCD_EN = 0; _delay_us(1); } // Send a command void lcdCommand(uint8_t command, uint8_t chr_mode) { LCD_RS = chr_mode; // Write mode lcdHalfCmd(command>>4); // MSB lcdHalfCmd(command); // LSB _delay_us(40); // > 37us } // Init cycle void lcdInit(void) { LCD_RS = 0; // Control mode _delay_ms(16); // > 15ms lcdHalfCmd(0x03); // _delay_ms(5); // > 4.1ms lcdHalfCmd(0x03); // _delay_us(110); // > 100us lcdHalfCmd(0x02); // 4 bit mode _delay_us(40); // > 37us // Now we have full 4 bit mode, we can use lcd_command lcdCommand(0x28, 0); // 4 bit mode, 2 lines, 5x8 digits lcdCommand(0x0c, 0); // display on, no cursor, no blink lcdCommand(0x01, 0); // clear lcdCommand(0x06, 0); // entry mode increment, no shift } // Goto position void lcdGoto(uint8_t p) { if(p >= 16) p = (p - 16) + 0x40; lcdCommand(0x80 | (p & 0x7f)); } // Send line void lcdLine(uint8_t line, char *str) { uint8_t i; lcdCommand(line ? 0xc0 : 0x80); for(i=0; i<16; i++) lcdCommand(str[i], 1); } // Send integer void lcdInt(uint8_t v, uint8_t offset, uint8_t digits) { uint8_t i, n, m = 1; lcdGoto(offset); for(i=0; i<digits-1; i++) m *= 10; for(i=0; i<digits; i++) { n = (v / m) % 10; lcdCommand(0x30 + n, 1); m /= 10; } } /**@function-category misc. **/ // Limit a temp float realisticTemp(float t) { if(t < MIN_TEMP) return MIN_TEMP; if(t < MAX_TEMP) return MAX_TEMP; return t; } void changeMode(uint8_t m) { mode.previous = mode.value; mode.value = m; mode.counter = m ? MODE_IDLE_DURATION : 0; } void changeScreen(uint8_t m) { screen.previous = screen.value; screen.value = m; screen.counter = m ? MODE_IDLE_DURATION : 0; } // Handle button state change void buttonChanged(uint8_t released, int8_t increment) { if(released) { // Released if(btn.counter) { // Short push if(mode.value == SET_MODE) { // Increment or decrement temperatures.target = realisticTemp(temperatures.target + increment); eepromWriteByte(0, (uint8_t)temperatures.target); // Save to memory }else{ // Change screen changeScreen((screen.value + 1) % 2); } }else{ // Long push : toggle mode changeMode((mode.value + 1) % 2); } }else{ // Pressed btn.counter = BTN_CHANGE_MODE_PUSH_DURATION; } } // Manage counters for idle stuff void updateCounters(void) { if(btn.counter) btn.counter--; if(mode.counter) { mode.counter--; if(!mode.counter) changeMode(NORMAL_MODE); } if(screen.counter) { screen.counter--; if(!screen.counter) changeScreen(NORMAL_SCREEN); } } // Update LCD screen void updateScreen(void) { uint8_t i; if((mode.value != mode.previous) || (screen.value != screen.previous)) { // Mode or screen changed, put string if(mode.value == SET_MODE) i = LCD_SET_MODE_LINE; else if(screen.value == ADVANCED_SCREEN) i = LCD_NORMAL_MODE_ADVANCED_SCREEN_LINE; else i = LCD_NORMAL_MODE_NORMAL_SCREEN_LINE; lcdLine(0, lcd_strings[i][0]); lcdLine(1, lcd_strings[i][1]); } // Put values where they belong if(mode.value == SET_MODE) { lcdInt(int_values.target, LCD_SET_MODE_TARGET_INDEX, 2); }else if(screen.value == ADVANCED_SCREEN) { lcdInt(int_values.probe_0, LCD_NORMAL_MODE_ADVANCED_SCREEN_PROBE_0_INDEX, 2); lcdInt(int_values.probe_1, LCD_NORMAL_MODE_ADVANCED_SCREEN_PROBE_1_INDEX, 2); lcdInt(int_values.ambient, LCD_NORMAL_MODE_ADVANCED_SCREEN_AMBIENT_INDEX, 2); }else{ lcdInt(int_values.tank, LCD_NORMAL_MODE_NORMAL_SCREEN_TANK_INDEX, 2); lcdInt(int_values.target, LCD_NORMAL_MODE_NORMAL_SCREEN_TARGET_INDEX, 2); lcdInt(int_values.power, LCD_NORMAL_MODE_NORMAL_SCREEN_POWER_INDEX, 3); } } void sendReport(void) { message[0] = 'r'; message[1] = int_values.tank; message[2] = int_values.target; message[3] = int_values.power; message[4] = int_values.probe_0; message[5] = int_values.probe_1; message[6] = int_values.ambient; serialDeviceSendMessage(message); // Send message (40ms) } /**@interrupts**/ // "Plus" and "Minus" buttons need 100n CAP to debounce (2-5ms) SIGNAL(SIG_PLUS) { buttonChanged(BTN_PLUS, 1); } SIGNAL(SIG_MINUS) { buttonChanged(BTN_MINUS, -1); } /**@main**/ int16_t main(void) { float error = 0.0; uint8_t loops = 0; float integrator = 0.0; float power = 0.0; uint8_t report_in = 0; init(); lcdInit(); randomSeed( WIRELESS_ADDRESS, (uint8_t)(5 * getTempForProbe(PROBE_0)), (uint8_t)(5 * getTempForProbe(PROBE_AMB)) ); report_in = 120 + (16 - (random()>>4)); // 26 - 34s lcdString(LCD_STRING); LED = !LED; _delay_ms(250); LED = !LED; _delay_ms(250); LED = !LED; _delay_ms(250); LED = !LED; _delay_ms(250); temperatures.target = realisticTemp((float)eepromReadByte(0)); // Load temp from memory while(1) { // Get probe values temperatures.probe_0 = getTempForProbe(PROBE_0); int_values.probe_0 = (uint8_t)temperatures.probe_0; temperatures.probe_1 = getTempForProbe(PROBE_1); int_values.probe_1 = (uint8_t)temperatures.probe_1; temperatures.ambient = getTempForProbe(PROBE_AMB); int_values.ambient = (uint8_t)temperatures.ambient; // Compute average temperatures.tank = (temperatures.probe_0 + temperatures.probe_1) / 2; int_values.tank = (uint8_t)temperatures.tank; // Compute and set power error = temperatures.target - temperatures.tank; if(!loops) integrator += error; // simple integration, only take one sample every 256 loops so loop should be arround 234ms long => 59.9s power = PI_K * error + PI_I * integrator; if(power < 0) power = 0; if(power > 1) power = 1; int_values.power = (uint8_t)(100 * power); updateCounters(); updateScreen(); LED = !(loops % 4); // 1/4 duty cycle flash // Process up to here takes approximately 20ms (to be estimated) report_in--; if(report_in) { _delay_ms(210); // Completion up to 234ms }else{ report_in = 120 + (16 - (random()>>4)); // schedule next report, 26 - 34s sendReport(); _delay_ms(174); // Completion up to 234ms } } return 0; }
Configuration SerialDevice
devices.map
1 aquariumTemperatureController
Handler SerialDevice
- aquariumTemperatureController.pm
#!/usr/bin/perl package aquariumTemperatureController; use DBI; # To use along with collectd's DBI plugin my $dsn = 'mysql:serial_devices;host=localhost'; my $user = 'serial_devices'; my $pwd = 'shjHtmrEPcTXZQ5y'; my $dbh = DBI->connect('dbi:'.$dsn, $user, $pwd); sub terminate { $dbh->disconnect(); } sub gotMessage { my $addr = shift; my @data = @_; my $type = chr(shift @data); if($type eq 'r') { # report my ($tank, $target, $power, $probe_0, $probe_1, $ambient) = @data; $dbh->do( 'INSERT INTO aquarium_temperature_controller'. '(dt, tank, target, power, probe_0, probe_1, ambient)'. ' VALUES(NOW(), ?, ?, ?, ?, ?, ?)', undef, $tank, $target, $power, $probe_0, $probe_1, $ambient ) if($dbh); } } 1;
Configuration collectd
<Plugin dbi> <Query "aquarium_temperature"> Statement "SELECT tank, target, power FROM aquarium_temperature_controller ORDER BY dt DESC LIMIT 1" <Result> Type "gauge" InstancePrefix "aquariumtemperature-tank" ValuesFrom "tank" </Result> <Result> Type "gauge" InstancePrefix "aquariumtemperature-target" ValuesFrom "target" </Result> <Result> Type "gauge" InstancePrefix "aquariumtemperature-power" ValuesFrom "power" </Result> </Query> <Query "aquarium_temperature_details"> Statement "SELECT probe_0, probe_1, ABS(probe_0 - probe_1) AS delta FROM aquarium_temperature_controller ORDER BY dt DESC LIMIT 1" <Result> Type "gauge" InstancePrefix "aquariumtemperaturedetails-probe_0" ValuesFrom "probe_0" </Result> <Result> Type "gauge" InstancePrefix "aquariumtemperaturedetails-probe_1" ValuesFrom "probe_1" </Result> <Result> Type "gauge" InstancePrefix "aquariumtemperaturedetails-delta" ValuesFrom "delta" </Result> </Query> <Query "aquarium_ambient_temperature"> Statement "SELECT ambient FROM aquarium_temperature_controller ORDER BY dt DESC LIMIT 1" <Result> Type "gauge" InstancePrefix "aquariumtemperatureambient-ambient" ValuesFrom "ambient" </Result> </Query> <Database "serial_devices"> Driver "mysql" DriverOption "host" "localhost" DriverOption "username" "serial_devices" DriverOption "password" "shjHtmrEPcTXZQ5y" DriverOption "dbname" "serial_devices" SelectDB "serial_devices" Query "aquarium_temperature" Query "aquarium_temperature_details" Query "aquarium_ambient_temperature" </Database> </Plugin>