User Tools

Site Tools


private:arietty:home

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

P = 10 * S * Delta T ???

S est la surface d'échange eau-air, le 10 est une constante de conduction en W/{m^2 °C}.

On prendra le pire Delta T 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 …

R = 12 / 15 = 0.8 Ohm

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.

Typon

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>
private/arietty/home.txt · Last modified: 2022/06/30 21:13 by 127.0.0.1