|
Forum Index : Microcontroller and PC projects : Using the Pi-cromite for projects
| Author | Message | ||||
| MikeO Senior Member Joined: 11/09/2011 Location: AustraliaPosts: 275 |
I mentioned in a recent post here , for my home weather station, that I had originally fully intended to use a Model Pi3B as the main controller but had run into so many reliability problems that I eventually used a MX470 instead, just so I could just get then project working and completed, Well working it is and rock solid with weather live data now being displayed here and also collected on my thingspeak channel . However since I finished this project it has been nagging me that the original plan to use a Pi was not successful and so problematic, especially seeing Peter has put so much time into it and is so responsive to requests for changes, bugs, and features. So I started looking at it again, I wanted to share here the problems I was having and what diagnostic tests I had done. I will say I find the Pi does present extra layers / hurdles which I am not totally confident in handling compared to a standard micromite which is another reason in raising this here, I would like to know what other contributors experiences have been, what kind of coding/projects they have tried, successful? or not, how they tackle fault finding, Etc. My experience outlined in the post above, was to use the Pi as a data processor with 3 main tasks. 1. Receiving (via WiFi UDP protocol) data from the wind sensor (speed and direction) 2. Read some local sensors (Temp, Rain, Pres, Hum), 3. finally collate all this data and send it to: Local network (UDP) and Thinkspeak channel. Quite a lot going on but it all worked well for an hour or so but then crashed and the Pi had to be power reset, very frustrating. I didn’t try many things I was a bit lost in fact, I had quite a few print statements in my code , my gut feeling was saying I should remove them or some at least and perhaps could have pulsed some digital outputs and viewed on a scope but I didn’t take that step. From memory it was pretty well the same code ( except the additional ESP8266 interface code) that was used on the MX470 so I don’t think the problem was the code pur se. The code is below. My tests again in past few days, used a very simple test routine , just receiving some UDP messages and every minute or so requesting som weather data from Weather Underground. I have a couple of model Pi3 boards so to rule out any hardware I used the second board and i also created a completely fresh SD card. This code also crashes after running around 1-2 hrs. Again my inclination is to remove print statements but I have not done this as yet as I was keen first to get some input or particularly other forum members experiences. Mike Simple recent code dim integer a(1000) dim l% on error skip udp close pause 1000 udp server 5001 Do l%=l%+1 UDP receive a1$,b$,c% 'Text 0,0,a1$ print a1$,b$,c% Pause 1000 if l%>60 then system "curl http://api.wunderground.com/api/1efa4dd621424929/conditions/q/AU/Melbourne.json -m 60",a() for i=1 to llen(a()) print lgetstr$(a(),i,1); next i l%=0 end if loop Original weather station code 'Michael Ogden Oct 2018 'Credits ' 'RPI Weather Station 'Change Log 'ver 0.05 beta 'ver 0.06b 'add in Wifi support Lib 3.76 '**************************** option explicit option autorun on 'OPTION SDCARD 49 if mm.watchdog then Print "Reset error" 'cpu 48 sub MM.STARTUP 'set Nowifi=1 if no ESP8266 'Nowifi=1 end sub 'BME280 routines and test harness const BME280_ADDRESS = &H77 const BME280_REGISTER_T1 = &H88 const BME280_REGISTER_P1 = &H8E const BME280_REGISTER_H1 = &HA1 const BME280_REGISTER_H2 = &HE1 const BME280_REGISTER_CHIPID = &HD0 const BME280_REGISTER_CONTROLHUMID = &HF2 const BME280_REGISTER_CONTROL = &HF4 const BME280_REGISTER_PRESSUREDATA = &HF7 const BME280_REGISTER_TEMPDATA = &HFA const BME280_REGISTER_HUMIDDATA = &HFD ' dim integer s16=&HFFFFFFFFFFFF0000 , s16b=&H8000 dim integer s12=&HFFFFFFFFFFFFF000 , s12b=&H800 dim integer s8= &HFFFFFFFFFFFFFF00 , s8b=&H80 ' DIM INTEGER T1,T2,T3 'uint16_t, int16_t, int16_t DIM INTEGER P1,P2,P3,P4,P5,P6,P7,P8,P9 'uint16_t, 8 x int16_t DIM INTEGER H1,H2,H3,H4,H5,H6 'uint8_t, int16_t , uint8_t, int16_t, int16_t, int8_t ' dim INTEGER t_fine 'used to store accurate temp reading from temp conversion for use in pressure and humidity conversions 'end bme280 setup ' general Variables dim string api_key$="9TQZQVPQKCSKZS50" dim string m$="api.thingspeak.com/update?api_key="+api_key$+"&field1=" 'dim wow$="192.168.0.116/template/data/update.php?" dim wow$="codenquilts.com.au/template/data/update?" dim string a1,b$,send$ dim Integer c,rainclk,secs dim Float otemp,pres,hum,rain,caseTemp ' 'Dim Flag(20) def in Library 'Flags '1=Thinkspeak upload '2=Wunderground upload '3=rain 'I/O SETPIN 11,INTl,rain_click,pullup' triggers on a Low setpin 29,dout,,pullup settick 1000,tick,1 'setpin 14, dout pin(29)=0 'Red Led 'Constants const node$="ESP_Weather" const serv$="ESP_Svr" 'Var dim integer a(1000) dim l% 'Initialise bme280_init 'initialise bme280 on error skip 'udp server 5001 'initialise UDP server TEMPR START 13,3 'initialise temp pause 100 'main loop Do '***** mxWiFi Hook **** Network only if userprocessflag=1 then 'StartTime = Timer UserProcess UserProcessFlag=0 xbsend "udp.send:|"+from$+"|"+chr$(4)+"|" 'Print "Processing elapsed time"; Timer-StartTime watchdog 10000 continue do 'skip to a new loop immediately , there may be more external processing to do endif '***** End mxWiFi Hook **** watchdog 10000 if get_udp()>0 then print a1$ if flag(3)=1 then rain=rainclk otemp=read_temp() casetemp=bme280_read_temp() pres=bme280_read_pressure() hum=bme280_read_humidity() print "Rain:" + str$(rain) print "Temperature:" +str$(otemp,2,1) print "bme280_Temp:" +str$(casetemp,2,1) print "Pressure:" +str$(pres,4,1) print "Humidity:" +str$(hum,2,1) print "Wind Speed:";str$(wspeed) print "Wind Dir:";str$(wdir) send$="&node=Weather&f1="+str$(otemp,2,1)+"&f2="+str$(hum,2,1)+"&f3="+str$(pres,4,1) send$=send$+"&f4="+str$(wspeed)+"&f5="+str$(wspeed)+"&f6="+str$(rain)+"&f7="+str$(rain) send$=send$+"&f8="+str$(wdir) 'send$=str$(otemp,2,1)+","+str$(hum,2,1)+","+str$(pres,4,1)+","+str$(wspeed)+","+str$(wspeed) 'send$=send$+send$+","+str$(rain)+","+str$(rain)+","+str$(wdir) 'UDP Network only transmission,data may be sent by other means xbsend "udp.cast:"+send$ WaitforReady 'reset flag 'xbsend "ts.send:"+tskey$+":"+str$(3)+":"+str$(wspeed)+":" ' WaitforReady 'reset flag send$=m$+str$(otemp,2,1)+"&field2="+str$(hum,2,1)+"&field3="+str$(wspeed)+"&field4="+str$(wdir) send$=send$+"&field5="+str$(pres,4,1)+"&field6="+str$(rain) xbsend "netsend:"+send$ print send$ WaitforReady 'reset flag flag(3)=0 pause 1000 end if ' if flag(4)=1 then ' send$="&f1="+str$(otemp,2,1)+"&f2="+str$(hum,2,1)+"&f3="+str$(pres,4,1) ' send$=send$+"&f4="+str$(wspeed)+"&f5="+str$(wspeed)+"&f6="+str$(rain)+"&f7="+str$(rain) ' send$=send$+"&f8="+str$(wdir) ' xbsend "netsend:"+send$ ' print send$ ' WaitforReady 'reset flag ' flag(4)=0 ' pause 1000 ' end if loop ' Subs and functions 'rain interupt sub rain_click rainclk=rainclk+1 pause 500 end sub 'one second interupt sub tick secs=secs+1 pulse 29,50 'Red led if secs mod 15 = 0 then flag(3)=1 'set sensor flag if secs mod 30 = 0 then flag(4)=1 'set sensor flag end sub 'read UDP port function get_udp() as integer 'UDP receive a1,b,c get_udp=c end function function read_temp() as float read_temp=1000 on error skip read_temp=tempr(13) end function function bme280_read_temp() as float local integer var1,var2,adc_T local adc%(2) i2c write BME280_ADDRESS,1,1,BME280_REGISTER_TEMPDATA i2c read BME280_ADDRESS,0,3,adc%() adc_T=((adc%(0)<<16) OR (adc%(1)<<8) or adc%(2))>>4 var1 = ((((adc_T>>3) - (T1 <<1))) * T2) \ q(11) var2 = (((((adc_T>>4) - (T1)) * ((adc_T\ q(4)) - (T1))) \ q(12)) * (T3)) \ q(14) t_fine = var1 + var2 bme280_read_temp = ((t_fine * 5 + 128) \ q(8))/100.0 end function function bme280_read_pressure() as float local integer var1, var2, adc_P, p local adc%(2) i2c write BME280_ADDRESS,1,1,BME280_REGISTER_PRESSUREDATA i2c read BME280_ADDRESS,0,3,adc%() adc_P=((adc%(0)<<16) OR (adc%(1)<<8) or adc%(2))>>4 var1 = t_fine - 128000 var2 = var1 * var1 * P6 var2 = var2 + ((var1 * P5)<<17) var2 = var2 + (P4 << 35) var1 = ((var1 * var1 * P3)\ q(8)) + ((var1 * P2)<<12) var1 = ((1<<47)+var1)*P1\ q(33) if var1 = 0 THEN bme280_read_pressure = 0' avoid exception caused by division by zero exit function endif p = 1048576 - adc_P p = (((p<<31) - var2)*3125) \ var1 var1 = (P9 * (p\ q(13)) * (p\ q(13))) \ q(25) var2 = (P8 * p) \ q(19) p = ((p + var1 + var2) \ q(8)) + (P7<<4) bme280_read_pressure = p/25600.0 end function ' function bme280_read_humidity() as float local integer v_x1,adc_H local adc%(1) i2c write BME280_ADDRESS,1,1,BME280_REGISTER_HUMIDDATA i2c read BME280_ADDRESS,0,2,adc%() adc_H=(adc%(0)<<8) or adc%(1) v_x1 = t_fine - 76800 v_x1=(((((adc_H<<14)-((H4)<<20)-(H5*v_x1))+16384)\ q(15))*(((((((v_x1*H6)\ q(10))*(((v_x1*H3)\ q(11))+32768))\ q(10))+2097152)*H2+8192)\ q(14))) v_x1 = (v_x1 - (((((v_x1 \ q(15)) * (v_x1 \ q(15))) \ q(7)) * (H1)) \ q(4))) if v_x1< 0 then v_x1 = 0 if v_x1 > 419430400 then v_x1= 419430400 bme280_read_humidity = (v_x1\ q(12)) / 1024.0 end function sub bme280_init local i%,cal%(17) i2c open 400,1000 '400KHz bus speed i2c write BME280_ADDRESS,1,1,BME280_REGISTER_CHIPID i2c read BME280_ADDRESS,0,1,i% if i%<>&H60 then print "Error BME280 not found" ' i2c write BME280_ADDRESS,1,1,BME280_REGISTER_T1 i2c read BME280_ADDRESS,0,6,cal%() T1=cal%(0) OR (cal%(1)<< 8) T2=cal%(2) OR (cal%(3)<< 8): if T2 and s16b then T2=T2 OR s16 'sign extend if required T3=cal%(4) OR (cal%(5)<< 8): if T3 and s16b then T3=T3 OR s16 ' i2c write BME280_ADDRESS,1,1,BME280_REGISTER_P1 i2c read BME280_ADDRESS,0,18,cal%() P1=cal%(0) OR (cal%(1)<<8) P2=cal%(2) OR (cal%(3)<<8): if P2 and s16b then P2=P2 OR s16 'sign extend if required P3=cal%(4) OR (cal%(5)<<8): if P3 and s16b then P3=P3 OR s16 P4=cal%(6) OR (cal%(7)<<8): if P4 and s16b then P4=P4 OR s16 P5=cal%(8) OR (cal%(9)<<8): if P5 and s16b then P5=P5 OR s16 P6=cal%(10) OR (cal%(11)<<8): if P6 and s16b then P6=P6 OR s16 P7=cal%(12) OR (cal%(13)<<8): if P7 and s16b then P7=P7 OR s16 P8=cal%(14) OR (cal%(15)<<8): if P8 and s16b then P8=P8 OR s16 P9=cal%(16) OR (cal%(17)<<8): if P9 and s16b then P9=P9 OR s16 ' i2c write BME280_ADDRESS,1,1,BME280_REGISTER_H1 i2c read BME280_ADDRESS,0,1,H1 i2c write BME280_ADDRESS,1,1,BME280_REGISTER_H2 i2c read BME280_ADDRESS,0,7,cal%() H2=cal%(0) OR (cal%(1)<< 8): if H2 and s16b then H2=H2 OR s16 'sign extend if required H3=cal%(2) H6=cal%(6): if H6 and s8b then H6=H6 OR s8 'sign extend if required H4=(cal%(3)<<4) OR (cal%(4) and &H0F): if H4 and s12b then H4=H4 OR s12 'sign extend if required H5=(cal%(5)<<4) OR (cal%(4)>>4): if H5 and s12b then H5=H5 OR s12 ' i2c write BME280_ADDRESS,0,2,BME280_REGISTER_CONTROLHUMID,&H05 '16x oversampling humidity i2c write BME280_ADDRESS,0,2,BME280_REGISTER_CONTROL,&HB7 '16x oversampling pressure/temp, normal mode ' end sub ' function q(x as integer) as integer 'returns 2 raised to the power q=(1<<x) End Function 'External command processing sub Routine if required, network only sub extCmd local st$ 'print "External Commands:";from$;",";cmd$;",";cfg$;",",strin$ if cmd$="ESP_Wind" then st$=strin$ print "Wind:";st$ wdir=val(parse$(st$,1,",")) wspeed=val(parse$(st$,4,",")) end if end Sub 'format: YYYY-mm-DD HH:mm:ss, where ':' is encoded as %3A, 'and the space is encoded as either '+' or %20. An example, 'valid date would be: 2011-02-29+10%3A32%3A55, 'for the 2nd of Feb, 2011 at 10:32:55. 'Note that the time is in 24 hour format. 'Also note that the date must be adjusted to UTC time - 'equivalent to the GMT time zone.. 'This is especially important for users outside the UK function dateutc() as string local string hh,min,ss,dd,mm,yy yy=right$(date$,4) mm=mid$(date$,4,2) dd=left$(date$,2) hh=left$(time$,2) min=mid$(time$,4,2) ss=right$(time$,2) dateutc=yy+"-"+mm+"-"+dd+"+"+hh+"%3A"+min+"%3A"+ss end function Codenquilts |
||||
| disco4now Guru Joined: 18/12/2014 Location: AustraliaPosts: 1044 |
Hi Mike, I have a weather station based on Pi3. It gets data via darksky api. I use a SYSTEM call to WGET to get the data and JSON to parse it. With latest MMbasic seems to run pretty reliably(days/weeks). (previous memory leaks in JSON command and calling SUB with a string now fixed) I use 'screen - provides terminal for current session so you can close putty. 'screen -D -r - lists screen sessions you can reconnect to to run it without the connected terminal. If it dies you can reconnected to see what happened. On another session look at what linux processes are running to get some clues. WGET is blocking so if it doesn't return it can stop. I have yet to try it with CURL as you previously suggested. I have a LCD screen attached so put some debug info on there, I don't print continually during each loop. I trouble shoot this type of thing by taking bits out until it no longer stops, then put them back until I find the culprit. How do your failures present, a) freeze - no response b) return to MMBasic prompt c) return to linux prompt d) what processes are running e.g. MMbasic, CURL How to you keep the MMBasic session alive. a) permanent connected terminal b) screen c) other. You could repeat this bit of code say 5 times in a row in your code and see if it affects the amount of time it will run to see it print is having an effect. for i=1 to llen(a()) print lgetstr$(a(),i,1); next i regards Gerry F4 H7FotSF4xGT |
||||
| MikeO Senior Member Joined: 11/09/2011 Location: AustraliaPosts: 275 |
Hi Gerry, thanks for your comments. I admit I am not totally confident with Linux but am aware of screen so will give that a go again. During the last tests I have been using the com port via the option console command. My failures I believe have all resulted in the Pi being frozen (even when I was using a session) so no possibility of logging in again anyway. As mentioned my gut feeling was to remove prints and see the result, so I must do that. Thanks again for your suggestions. Mike Codenquilts |
||||
| matherp Guru Joined: 11/12/2012 Location: United KingdomPosts: 10572 |
Mike Do you have an LCD connected? What power supply are you using - Pis are very sensitive to poor power? Is it overheating - MMBasic runs at 100% on one CPU? Which version of the Pi? Raspbian Lite or full? Stretch or Jessie? Are you using the serial console or an SSH console? I'm running your test program with another Pi-cromite sending UDP messages every 1.5 seconds. At 1 hour so far and still OK. UPDATE 2 hours and all OK running Jessie Raspbian Lite on Pi-Zero, serial console on com 1. The UDP sender is Full Raspbian Stretch on Pi3, MMBasic on SSH terminal. UPDATE Over 3 hours - getting bored. No sign of memory usage expanding or other issues. It is critical if you are using a SSH terminal that it stays connected or you use a mechanism like "screen" so the the process sees a connection and print output has somewhere to go. Or just dump all output: sudo ./mmbasic &>/dev/null & then if started over SSH you will need to kill the two processes by hand: ps - aux sudo kill -9 PID1 sudo kill -9 PID2 UPDATE Over 7 hours - still good |
||||
| Paul_L Guru Joined: 03/03/2016 Location: United StatesPosts: 769 |
Hey Mike, I can't reach your codenquilts server from NY. All browsers time out. The temperature display from the thingspeak site indicates that you have not moved another 1000 km south. I don't understand the timeout. A ping to TBS takes about 70 ms from here. Paul in NY |
||||
| MikeO Senior Member Joined: 11/09/2011 Location: AustraliaPosts: 275 |
Paul, Looks like the Synology NAS fell over during the night around 4.20 am my time (~ midday Sat your time) It does a backup some time before that ? anyway its up again now. The Thingspeak channel should have been getting data. Peter, Plenty for me to check out there. Yes power supply, I will try out another, I had of course considered it but had made some measurements and under load it was delivering 5.16vdc on the Pi, hardly moved. I have only the basic Pi with a small lcd attached. 5v supply was rated at 2A. Its not overheating, measures ~42C, Running Latest Raspian Stretch full but only in SSH mode from WinSCP. This last time I was running the serial console but had started the session from ssh via WinSCP, maybe the session fell over. Its obvious I need to set up a more strategic test. Will report back. Codenquilts |
||||
| Paul_L Guru Joined: 03/03/2016 Location: United StatesPosts: 769 |
Hmmmm. I can now reach your weather data display which looks really great, but the link below your signature on each post still times out. I think that Synology NAS is having some persistent problems. Paul in NY |
||||
| MikeO Senior Member Joined: 11/09/2011 Location: AustraliaPosts: 275 |
Hi Paul, OK I checked and when I hovered over it it looked like the port number in the address may be missing (although it was in the signature profile ?), the full address should be http://codenquilts.synology.me:2244/cms/index.php , the port number is required as this NAS is behind my home router and my ISP blocks the use of the "HTTP" port 80(like many residential ISPs do) so I have to use another port number and re direct to port 80 in the router, otherwise it will not connect, If you wish please try again. Mike Codenquilts |
||||
| Paul_L Guru Joined: 03/03/2016 Location: United StatesPosts: 769 |
Hi Mike. The link you posted, http://codenquilts.synology.me:2244/cms/index.php, is the same as what I see when I hover over the link. Neither one works, they just time out. This has been like this for months. I remember trying to access your web site a while ago and the same thing happened. Paul in NY |
||||
| JohnS Guru Joined: 18/11/2011 Location: United KingdomPosts: 4133 |
The http://codenquilts.synology.me:2244/template/indexDesktop.php one works for me as does the Thingspeak one at https://thingspeak.com/channels/17394. The 3rd, at http://codenquilts.synology.me:2244/cms/index.php just hangs. john |
||||
| lew247 Guru Joined: 23/12/2015 Location: United KingdomPosts: 1702 |
How do you actually use the screen command? Is it a Linux or MM command? I've been using the Pi Zero W headless with wireless connection so far, I've just got it working with the usb cable connected and I'm trying to get MM running so I can disconnect the cable and leave the weather station running. |
||||
| disco4now Guru Joined: 18/12/2014 Location: AustraliaPosts: 1044 |
To use screen You probably need to install it with sudo apt-get install screen at the linux command. Then type screen before you start up MMBASIC It will stay running once you close the terminal. To reconnect, you type screen -D -r at the linux prompt when you log back in, you then reconnect to the still running screen session. It will get the session back regardless of whether you are on a remote or USB login and regardless of how you logged in when you initially started it. F4 H7FotSF4xGT |
||||
| MikeO Senior Member Joined: 11/09/2011 Location: AustraliaPosts: 275 |
I can report that the pi-cromite code has been running now for ~3 days without a hiccup, same power supply so the problem was as you suspected the session was timing out when it wanted to print. I have no prints now and running in console mode (but nothing connected) , autorun and has a small LCD showing the received data. Thanks all for you help I can now move on with any projects using Pi-cromite with more confidence. Also my NAS seems to be accessible from the Internet OK I have tested via my Cell Phone (so outside my local wifi network) we have had a few storms this week however and our Hi speed wireless internet service does tend to suffer in bad and rainy weather, thanks for your reports anyway. Mike Codenquilts |
||||
| Paul_L Guru Joined: 03/03/2016 Location: United StatesPosts: 769 |
Hi Mike, still no connection. The connection has timed out The server at codenquilts.synology.me is taking too long to respond. The site could be temporarily unavailable or too busy. Try again in a few moments. Paul in NY |
||||
| matherp Guru Joined: 11/12/2012 Location: United KingdomPosts: 10572 |
Thanks to disco4now and google I've finally sorted a way of running a MMBasic program fully automatically at boot on a Pi - the steps are pretty simple (once you know how ). There may be better ways but this is how I got it working. The following assumes that the mmbasic executable is in the directory /home/pi. This is the default login directory for the username "pi".First install the application "screen" on the pi sudo apt-get install screen Next create a file "start.bash" in the same directory as mmbasic. Put the following into the file cd /home/pi sudo ./mmbasic Make the script executable with the command chmod +x start.bash Now we need to tell Raspbian to execute screen and start.bash on boot. Edit the file "/etc/rc.local" (sudo nano /etc/rc.local) and append the following line to the file /usr/bin/sudo -u pi /usr/bin/screen -dmS screen /home/pi/start.bash This tells the pi to run screen under the user "pi" and to execute "start.bash" under screen. Next, we assume we want to run a specific MMBasic program so once it is developed save it as AUTORUN.BAS in the same directory as the mmbasic executable. Run MMBasic as normal (sudo ./mmbasic) and set the autorun option OPTION AUTORUN ON exit mmbasic (ctrl-Z) Finally reboot the pi (sudo shutdown -r now") and when it reboots your MMBasic program will be running - you can check this with the command "ps -aux" To connect to your running program execute the command screen -D -r and the terminal will connect so you can see any output and stop the program and edit it if required. The use of "screen" ensures that console output from MMBasic is properly handled and your program can run happily without a console connected. I'll include this as an appendix in the next release of the Pi-cromite manual Attached is a simple program to email me a picture from a camera on a pi-zero every hour that now runs AUTOMATICALLY SetTick 60000,ping Do takepicture Pause 180000 Loop Sub ping System "ping 8.8.8.8 -c 1" ' keep the network alive End Sub Sub takepicture Local fn$, a$ a$="raspistill -o " fn$="PIC"+Right$(Date$,4)+Mid$(Date$,4,2)+Left$(Date$,2) fn$=fn$+Left$(Time$,2)+Mid$(Time$,4,2)+".jpg" System a$+fn$ Print "Taken picture "+fn$ a$="mpack -s "+fn$+" "+fn$+" peter@mather.host" System a$ Print "Emailed picture "+fn$ Kill fn$ End Sub |
||||
| disco4now Guru Joined: 18/12/2014 Location: AustraliaPosts: 1044 |
One minor issue I have found with screen is that if you paste your code into mmbasic with putty it sometimes will miss a character when pasting if hosted under screen. (especially if a large program). You can drop out of screen from the linux prompt using exit and run mmbasic without it during code development if pasting the code becomes a problem during development. F4 H7FotSF4xGT |
||||
| The Back Shed's forum code is written, and hosted, in Australia. | © JAQ Software 2025 |