Home
JAQForum Ver 24.01
Log In or Join  
Active Topics
Local Time 01:17 26 Nov 2024 Privacy Policy
Jump to

Notice. New forum software under development. It's going to miss a few functions and look a bit ugly for a while, but I'm working on it full time now as the old forum was too unstable. Couple days, all good. If you notice any issues, please contact me.

Forum Index : Microcontroller and PC projects : Silicon Chip Battery multi logger Feb/March 2021

Author Message
jockmack
Newbie

Joined: 17/03/2024
Location: Australia
Posts: 5
Posted: 04:09am 20 Mar 2024
Copy link to clipboard 
Print this post

Hello to everyone, I am new to the group but have a problem with a project I am unsure of how to solve.

I recently built the SC project "Battery multi logger".
Its based on micromite type design using PIC32MX170, it uses AD7192 four channel 24 bit ADC with SPI interface. A pic 16F1455 also provide USB to serial interface.

The problem is it works but I am unable to calibrate the current settings for the inputs. I have included some photos to show the interface on the LCD touchscreen ILI9341.

I was able to calibrate the volts OK and it is working fine but when trying to save any of the current values it appears to lock up the display. I don't believe the PIC is locked up as when the display is frozen I can still access the PIC and interrogate it via the USB  port and Tera term. Only a power cycle will clear the display.














I have reloaded the hex image as per Silicon Chip download but the behaviour was the same.
The images are in sequence, when you accept the value entered another OK Cancel buttons are displayed below the original OK Cancel buttons.Selecting OK locks up but Cancel will take you back to the calibration selection display.

The original basic code is as below.

I would expect there is no bugs as SC would have tested and I haven't seen any errata
or notes regarding this project.

One other anomaly I noticed was when initially setting up the touch screen (2.8"v1.2)buttons were mirror imaged as to the touch sensor, I did a GUI calibrate and it flipped the touch sensor but it did complain it with Warning: inaccurate calibration 0, 3937, 3855, -904, -544. original setting was 0, 192, 230, 908, 687.as per SC provided PIC32MX170.

If someone could point me to where I should look I would be grateful.

Thanks

Jock


 OPTION EXPLICIT
 OPTION BASE 0
 OPTION AUTORUN ON
 
 CONST BG_COL = RGB(BLACK)
 CONST FG_COL = RGB(WHITE)
 CONST FADE_COL = RGB(128,128,128)
 CONST BUTTON_COL = RGB(CYAN)
 CONST ERROR_COLOUR = RGB(RED)
 CONST HL_COLOUR = RGB(BLACK)
 CONST AXIS_COLOUR = RGB(128,128,128)
 CONST AXIS_LEFT = 34
 CONST AXIS_RIGHT = 286
 CONST AXIS_TOP = 36
 CONST AXIS_BOTTOM = 190
 CONST V_COLOUR = RGB(RED)
 CONST I1_COLOUR = RGB(YELLOW)
 CONST I2_COLOUR = RGB(GREEN)
 CONST I3_COLOUR = RGB(CYAN)
 CONST I4_COLOUR = RGB(BLUE)
 
 ''using Micromite % units
 Dim Integer ON_BL
 Dim Integer IDLE_BL
 Dim Integer MN_TMOUT
 CONST IDLE_TIMEOUT = 5000
 DIM INTEGER DISPLAY_TIMEOUT
 ON_BL = 70
 IDLE_BL = 5
 MN_TMOUT = 55000
 
 '' AD7192 interface
 '' using HW SPI on 3/25/14 (MOSI/SCK/MISO)
 '' ~5MHz max frequency
 '' full scale = 2^24 (16777216)
 '' 2.5V reference with 39/1 divider => FS=100V
 '' nominal value of Vx_CAL is 100/16777216
 '' 15m# shunts for current
 const ADC_CS=24
 DIM FLOAT V_CAL(5)    'four multipliers by raw 24bit ADC value
 V_CAL(1)=100/16777216 ''nominal 100V/24bit full scale
 V_CAL(2)=V_CAL(1)
 V_CAL(3)=V_CAL(1)
 V_CAL(4)=V_CAL(1)
 'DIM FLOAT V_ACT(5),V_SAMPLE(5)    'four readings
 SETPIN ADC_CS,DOUT    ''this stays on at all times to keep CS high
 pin(ADC_CS)=1
 CONST ADC_TIMEOUT = 1200  ''allows four samples/five seconds, typically takes 850ms/sample
 
 '' analog read on pin 4 for buck reg current shunt
 CONST SHUNT_ADC=4
 SETPIN SHUNT_ADC,AIN
 DIM FLOAT I_CAL(5)
 I_CAL(1) = -1/(100*0.1) '0.1# shunt with x100 gain same as 10# resistor. 1V => 100mA, result in A
 I_CAL(2) = -1/0.015  'nominal 15m# shunt
 I_CAL(3) = -1/0.015  'nominal 15m# shunt
 I_CAL(4) = -1/0.015  'nominal 15m# shunt
 
 DIM FLOAT I_RATIO(5)  ''used in alternate calibration, are ratio of I dividers to main V divider
 ''calculated on each sample ready for calibration, so V_CAL(2)=I_RATIO(2)*V_CAL(1) etc
 I_RATIO(2)=1
 I_RATIO(3)=1
 I_RATIO(4)=1
 
 ' globals used by Sub DrawButton
 CONST BUTTON_COUNT = 30
 Dim Integer key_coord(BUTTON_COUNT, 5)
 Dim String key_caption(BUTTON_COUNT)
 DIM INTEGER BUTTON_PRESS
 
 CONST SMOOTHING_FACTOR = 0  'number of samples over which data is exponentially smoothed ((OLD * SF)+ NEW)/(SF+1) 0=> no smoothing
 
 ''globals used as statics within subs
 ''sampling plan:
 ''channels take about a second each, so say 5s per scan, gives 12/min, 720/hr (hourly good for long term) ~2 days
 ''5 variables: Voltage and 4 currents
 
 DIM FLOAT NEW_RAW(10) 'for processing and smoothing
 NEW_RAW(0)=0
 NEW_RAW(1)=-1
 NEW_RAW(2)=0
 NEW_RAW(3)=0
 NEW_RAW(4)=0
 NEW_RAW(5)=0
 NEW_RAW(6)=0
 NEW_RAW(7)=0
 NEW_RAW(8)=0
 NEW_RAW(9)=0
 
 DIM INTEGER RAW_ACTUAL(6) 'raw smoothed ADC results
 RAW_ACTUAL(0)=0 'ignore 0 index
 RAW_ACTUAL(1)=-1 'voltage, set as error to flag at startup
 RAW_ACTUAL(2)=0 'current 1
 RAW_ACTUAL(3)=0 'current 2
 RAW_ACTUAL(4)=0 'current 3
 RAW_ACTUAL(5)=0 'current 4
 
 ''actual now values, in units of volts/amps- all floats are these units
 DIM FLOAT ACTUAL(6)
 ACTUAL(0)=0 'ignore 0 index
 ACTUAL(1)=0 'voltage
 ACTUAL(2)=0 'current 1
 ACTUAL(3)=0 'current 2
 ACTUAL(4)=0 'current 3
 ACTUAL(5)=0 'current 4
 
 ''for capacity checking
 DIM FLOAT V_FULL,V_EMPTY,I_SCALE,V_SHUTDOWN
 ''defaults for 12V
 V_FULL=14.4
 V_EMPTY=11.0
 V_SHUTDOWN=10.5
 I_SCALE=20
 
 ''usage tracking
 DIM FLOAT AH_SINCEF, AH_SINCEE, WH_SINCEF, WH_SINCEE
 DIM INTEGER USAGE_STATE
 AH_SINCEF=0
 AH_SINCEE=0
 WH_SINCEF=0
 WH_SINCEE=0
 USAGE_STATE=0 ' +1= min reached, +2=max reached => 3=data complete
 
 ''these are at the limit of VAR SAVE
 CONST S_COUNT = 720
 CONST H_COUNT = 48
 CONST D_COUNT = 17
 DIM INTEGER SAMPLE_STATE, SAMPLE_PTR
 SAMPLE_STATE=0
 SAMPLE_PTR=0 ''(is index into 720 below)
 ''these arrays are filled from the start and then 'scrolled'
 DIM INTEGER D_START, D_PTR  ''what is day of first item in D_SAMP (format?), pointer into D_SAMP array
 DIM INTEGER H_START, H_PTR  ''what is day of first item in H_SAMP (format?), pointer into H_SAMP array
 D_PTR=0
 H_PTR=0
 ''to be set by RTC or when needed
 D_START=0
 H_START=0
 DIM FLOAT SAMPLES(5,S_COUNT)  ''an hour of samples, this takes 17k of RAM, not saved
 DIM FLOAT H_SAMP(5,H_COUNT)  ''2 days of hourly samples (takes 1189 bytes in VAR SAVE)
 DIM FLOAT D_SAMP(5,D_COUNT)   ''32 days of daily samples (takes 709 bytes)
 
 ''loop counters etc
 DIM I AS INTEGER
 ''mark values as invalid
 ''currents can be +/- so use V as flag
 FOR I = 0 to S_COUNT-1
   SAMPLES(0,I)=-1
   SAMPLES(1,I)=0
   SAMPLES(2,I)=0
   SAMPLES(3,I)=0
   SAMPLES(4,I)=0
 NEXT I
 FOR I = 0 to H_COUNT-1
   H_SAMP(0,I)=-1
   H_SAMP(1,I)=0
   H_SAMP(2,I)=0
   H_SAMP(3,I)=0
   H_SAMP(4,I)=0
 NEXT I
 FOR I = 0 to D_COUNT-1
   D_SAMP(0,I)=-1
   D_SAMP(1,I)=0
   D_SAMP(2,I)=0
   D_SAMP(3,I)=0
   D_SAMP(4,I)=0
 NEXT I
 
 ''display state management
 DIM INTEGER DISPLAYSTATE
 DISPLAYSTATE=2 'on
 
 ''temp for calculation and display
 DIM INTEGER DISP_NUM
 
 ''SETUP
 VAR RESTORE
 VAR SAVE H_SAMP(),D_SAMP(),D_START, D_PTR,H_START, H_PTR, V_CAL(), I_CAL(),ON_BL,IDLE_BL,MN_TMOUT,V_EMPTY,V_FULL,AH_SINCEF, AH_SINCEE, WH_SINCEF, WH_SINCEE,I_SCALE,V_SHUTDOWN
 
 LCD_ON  ''turn on LCD
 CLS(BG_COL)
 SET_BACKLIGHT(ON_BL)
 ''check RTC, ignore errors
 ON ERROR IGNORE
 RTC GETTIME
 ON ERROR ABORT
 DISPLAY_TIMEOUT=TIMER     ''this is used to monitor our display timeout
 
 DRAW_MAIN
 DO
   ''main loop, calls other routines as needed, they should DRAW_MAIN before returning
   SAMPLE_MANAGER    ''check sampling
   SAMPLE_MANAGER    ''do this twice as every second event happens quickly
   ''minute skipper for testing
   ''IF val(mid$(TIME$,4,2))<59 and val(mid$(TIME$,7,2))> 10 THEN SET_RTC(4,59):SET_RTC(5,45)  ''if minutes =1 , change to 59,so we can scroll through time quickly
   ''check buttons
   if DISPLAYSTATE>0 THEN
     'debug data for display timeout
     TEXT 315,0,right$("        "+str$(INT(((MN_TMOUT+IDLE_TIMEOUT)-(TIMER-DISPLAY_TIMEOUT))\1000)),8),RT,1,1,FG_COL,BG_COL
     'debug data for full/empty state 3=both states reached
     'TEXT 0,0,right$("   "+str$(USAGE_STATE),3),LT,1,1,FG_COL,BG_COL
     IF CHECK_SCREEN_TOUCH()=1 THEN
       DISPLAY_TIMEOUT=TIMER ''counts time awake, reset if screen touched
       IF DISPLAYSTATE<2 THEN
         DISPLAYSTATE=2
         SET_BACKLIGHT(ON_BL)
       ENDIF
     ENDIF
     BUTTON_PRESS=CheckButtonPress(0, 2)
     IF BUTTON_PRESS >= 0 THEN
       ''wake up/set full backlight on button press in case we missed it
       DISPLAYSTATE=2
       SET_BACKLIGHT(ON_BL)
       if BUTTON_PRESS=0 THEN
         CheckButtonRelease(0)
         DATA_MENU
       ENDIF
       if BUTTON_PRESS=1 THEN
         CheckButtonRelease(0)
         SETTINGS_MENU
       ENDIF
       if BUTTON_PRESS=2 THEN
         CheckButtonRelease(0)
         CALIBRATE_MENU
       ENDIF
     ENDIF
     ''display voltages etc on idle screen
     TEXT 160,68,"   "+DATE$+" "+TIME$+"   ",CT,1,1,FG_COL,BG_COL
     IF RAW_ACTUAL(1)<0 THEN ''sampling error
       TEXT 45,84,"ERROR",CT,1,1,ERROR_COLOUR,BG_COL
     ELSE
       TEXT 45,84,"  OK  ",CT,1,1,FG_COL,BG_COL
     ENDIF
     TEXT 4,100,"V="+STR$(ACTUAL(1), 5, 2)+"V",LT,1,1,FG_COL,BG_COL
     TEXT 4,116,"I1="+STR$(ACTUAL(2), 4, 2)+"A",LT,1,1,FG_COL,BG_COL
     TEXT 4,132,"I2="+STR$(ACTUAL(3), 4, 2)+"A",LT,1,1,FG_COL,BG_COL
     TEXT 4,148,"I3="+STR$(ACTUAL(4), 4, 2)+"A",LT,1,1,FG_COL,BG_COL
     TEXT 4,164,"I4="+STR$(ACTUAL(5), 4, 2)+"A",LT,1,1,FG_COL,BG_COL
     IF V_FULL - V_EMPTY > 0.1 THEN
       DISP_NUM = ((ACTUAL(1)-V_EMPTY)*100)/(V_FULL - V_EMPTY)
       if DISP_NUM<0 THEN DISP_NUM=0
       IF DISP_NUM>100 THEN DISP_NUM=100
       TEXT 112,84,"CHGv="+STR$(DISP_NUM, 3, 0)+"%",LT,1,1,FG_COL,BG_COL
     ELSE
       TEXT 112,84,"CHGv=---%",LT,1,1,ERROR_COLOUR,BG_COL
     ENDIF
     
     TEXT 112,116,"Ah since full= "+STR$(AH_SINCEF, 6, 1)+"Ah",LT,1,1,FG_COL,BG_COL
     TEXT 112,132,"Ah since empty="+STR$(AH_SINCEE, 6, 1)+"Ah",LT,1,1,FG_COL,BG_COL
     TEXT 112,148,"Wh since full= "+STR$(WH_SINCEF, 6, 1)+"Wh",LT,1,1,FG_COL,BG_COL
     TEXT 112,164,"Wh since empty="+STR$(WH_SINCEE, 6, 1)+"Wh",LT,1,1,FG_COL,BG_COL
     
     DISP_NUM=0
     'use this one to display capacity based on AH
     'IF (AH_SINCEF-AH_SINCEE)>0.1 THEN DISP_NUM = (-AH_SINCEE*100)/(AH_SINCEF-AH_SINCEE)
     'use this one to display capacity based on WH
     IF (WH_SINCEF-WH_SINCEE)>0.1 THEN DISP_NUM = (-WH_SINCEE*100)/(WH_SINCEF-WH_SINCEE)
     if DISP_NUM<0 THEN DISP_NUM=0
     IF DISP_NUM>100 THEN DISP_NUM=100
     IF USAGE_STATE>2 THEN
       TEXT 200,84,"CAP="+STR$(AH_SINCEF-AH_SINCEE, 6, 1)+"Ah",LT,1,1,FG_COL,BG_COL
       TEXT 200,100,"CAP="+STR$(WH_SINCEF-WH_SINCEE, 6, 1)+"Wh",LT,1,1,FG_COL,BG_COL
       TEXT 112,100,"CHGm="+STR$(DISP_NUM, 3, 0)+"%",LT,1,1,FG_COL,BG_COL
     ELSE
       TEXT 200,84,"CAP="+STR$(AH_SINCEF-AH_SINCEE, 6, 1)+"Ah",LT,1,1,FADE_COL,BG_COL
       TEXT 200,100,"CAP="+STR$(WH_SINCEF-WH_SINCEE, 6, 1)+"Wh",LT,1,1,FADE_COL,BG_COL
       TEXT 112,100,"CHGm="+STR$(DISP_NUM, 3, 0)+"%",LT,1,1,FADE_COL,BG_COL
     ENDIF
     ''update brightness and dim as needed
     ''fade
     IF (DISPLAYSTATE > 1) and ((TIMER-DISPLAY_TIMEOUT)>MN_TMOUT) THEN
       DISPLAYSTATE=1
       SET_BACKLIGHT(IDLE_BL)
     ENDIF
     ''off
     IF ((TIMER-DISPLAY_TIMEOUT)>MN_TMOUT+IDLE_TIMEOUT) THEN
       DISPLAYSTATE=0
       SET_BACKLIGHT(0)
       LCD_OFF
     ENDIF
     PAUSE 50  ''wait a little between updates
   ELSE  ''wait for touches and reset
     IF ACTUAL(1)> V_SHUTDOWN THEN
       CPU SLEEP 1     ''this is how we get very low power
     ELSE
       CPU SLEEP 15    ''longer shutdown for low power, ADC goes to sleep after conversion
     ENDIF
     IF CHECK_SCREEN_TOUCH()=1 THEN
       DISPLAY_TIMEOUT=TIMER ''counts time awake, reset if screen touched
       DISPLAYSTATE=2  ''fully awake
       LCD_ON
       CLS(BG_COL)    ''light up to let know that it's sensed the touch
       SET_BACKLIGHT(ON_BL)
       WAIT_FOR_RELEASE
       DRAW_MAIN                 ''draw display when touch released
     ENDIF
   ENDIF
 LOOP
 WATCHDOG 1 'in case we get here, restart
END
 
SUB CALIBRATE_MENU
 DRAW_CALIBRATE
 DO
   SAMPLE_MANAGER
   BUTTON_PRESS=CheckButtonPress(0, 5)
   IF BUTTON_PRESS >= 0 THEN
     CheckButtonRelease(BUTTON_PRESS)
     IF BUTTON_PRESS = 0 THEN  'volts
       IF OK_CANCEL_BOX("ENSURE NO LOAD","ON TERMINALS")>0 THEN DO_V_CAL
       DRAW_CALIBRATE       'return
     ENDIF
     IF (BUTTON_PRESS >=1) AND (BUTTON_PRESS <=4) THEN  'I1
       DO_I_CAL(BUTTON_PRESS)
       DRAW_CALIBRATE       'return
     ENDIF
     IF BUTTON_PRESS = 5 THEN EXIT DO
   ENDIF
 LOOP
 DRAW_MAIN
END SUB
 
SUB SETTINGS_MENU
 DRAW_SETTINGS
 LOCAL FLOAT NUM
 DO
   TEXT 160,30,"   "+DATE$+" "+TIME$+"   ",CT,1,1,FG_COL,BG_COL
   SAMPLE_MANAGER
   BUTTON_PRESS=CheckButtonPress(0, 13)
   IF BUTTON_PRESS >= 0 THEN
     CheckButtonRelease(BUTTON_PRESS)
     IF BUTTON_PRESS = 0 THEN  'year
       NUM=NUMBERPAD("Enter year:")
       IF NUM >= 2000 THEN SET_RTC(0,NUM)
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 1 THEN  'month
       NUM=NUMBERPAD("Enter month:")
       IF (NUM >= 1) AND (NUM <= 12) THEN SET_RTC(1,NUM)
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 2 THEN  'day
       NUM=NUMBERPAD("Enter day number:")
       IF (NUM >= 1) AND (NUM <= 31) THEN SET_RTC(2,NUM)
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 3 THEN  'hour
       NUM=NUMBERPAD("Enter hour (24):")
       IF (NUM >= 1) AND (NUM <= 24) THEN SET_RTC(3,NUM)
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 4 THEN  'minute
       NUM=NUMBERPAD("Enter minute:")
       IF (NUM >= 1) AND (NUM <= 60) THEN SET_RTC(4,NUM)
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 5 THEN  'second
       NUM=NUMBERPAD("Enter second:")
       IF (NUM >= 1) AND (NUM <= 60) THEN SET_RTC(5,NUM)
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 6 THEN  'BL full
       NUM=NUMBERPAD("Enter Backlight%:")
       IF (NUM >= 1) AND (NUM <= 100) THEN ON_BL=NUM:SET_BACKLIGHT(ON_BL):VAR SAVE ON_BL
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 9 THEN  'BL dim
       NUM=NUMBERPAD("Enter Backlight%:")
       IF (NUM >= 1) AND (NUM <= 100) THEN IDLE_BL=NUM:VAR SAVE IDLE_BL
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 8 THEN  'BL timeout
       NUM=NUMBERPAD("Enter timeout(s):")
       IF (NUM*1000 > IDLE_TIMEOUT) THEN MN_TMOUT=NUM*1000-IDLE_TIMEOUT : VAR SAVE MN_TMOUT
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 7 THEN  'Vfull
       NUM=NUMBERPAD("Enter V(full):")
       IF (NUM>=5) AND (NUM>V_EMPTY) AND (NUM>V_SHUTDOWN) THEN
         V_FULL=NUM
         VAR SAVE V_FULL
       ELSE
         MessageBox("V(full) too low","Check and retry")
       ENDIF
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 10 THEN  'Vempty
       NUM=NUMBERPAD("Enter V(empty):")
       IF (NUM>=5) AND (NUM<V_FULL) AND (NUM>V_SHUTDOWN) THEN
         V_EMPTY=NUM
         VAR SAVE V_EMPTY
       ELSE
         MessageBox("V(empty) too low","or above V(full)")
       ENDIF
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 12 THEN  'Iscale
       NUM=NUMBERPAD("Enter I(scale):")
       IF (NUM>=1) THEN
         I_SCALE=NUM
         VAR SAVE I_SCALE
       ELSE
         MessageBox("I(scale)","too low")
       ENDIF
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 13 THEN  'Vshutdown
       NUM=NUMBERPAD("Enter V(shutdown):")
       IF ((NUM >= 0) AND (NUM<V_EMPTY)) THEN
         V_SHUTDOWN=NUM
         VAR SAVE V_SHUTDOWN
       ELSE
         MessageBox("V(shutdown)","too high")
       ENDIF
       DRAW_SETTINGS       'return
     ENDIF
     IF BUTTON_PRESS = 11 THEN EXIT DO 'back
   ENDIF
 LOOP
 DRAW_MAIN
END SUB
 
SUB SET_RTC(S AS INTEGER, P AS INTEGER) '' sets parameter s to p (y=0,m=1,d=2,h=3,m=4,s=5) as per RTC SETTIME
 LOCAL INTEGER T(6)
 T(0)=val(mid$(DATE$,7,4)) 'year
 T(1)=val(mid$(DATE$,4,2)) 'month
 T(2)=val(mid$(DATE$,1,2)) 'day
 T(3)=val(mid$(TIME$,1,2)) 'hour/24
 T(4)=val(mid$(TIME$,4,2)) 'minute
 T(5)=val(mid$(TIME$,7,2)) 'second
 IF (S>=0) AND (S<=5) THEN T(S)=P'validation must occur before calling
 ON ERROR IGNORE
 RTC SETTIME T(0),T(1),T(2),T(3),T(4),T(5)
 ON ERROR ABORT
END SUB
 
SUB DATA_MENU
 LOCAL INTEGER GRAPH_MODE
 LOCAL INTEGER UPDATE_TIME
 UPDATE_TIME=TIMER
 GRAPH_MODE=0
 DRAW_DATA
 DRAW_GRAPH(GRAPH_MODE)
 DO
   SAMPLE_MANAGER
   BUTTON_PRESS=CheckButtonPress(0, 4)
   IF BUTTON_PRESS >= 0 THEN
     CheckButtonRelease(BUTTON_PRESS)
     IF BUTTON_PRESS < 3 THEN GRAPH_MODE=BUTTON_PRESS: DRAW_GRAPH(GRAPH_MODE): UPDATE_TIME=TIMER
     IF BUTTON_PRESS = 3 THEN EXIT DO
     IF BUTTON_PRESS = 4 THEN EXPORT_DATA(GRAPH_MODE)
   ENDIF
   'check for touches on scale and display
   IF (TIMER - UPDATE_TIME) > 60000 THEN DRAW_GRAPH(GRAPH_MODE): UPDATE_TIME=TIMER   ''update every minute
 LOOP
 DRAW_MAIN
END SUB
 
SUB DRAW_GRAPH(N AS INTEGER)  ''0=hours, 1=days, 2=weeks
 LOCAL INTEGER I
 ''clear chart area
 BOX 0,AXIS_TOP,319,AXIS_BOTTOM-AXIS_TOP+1,,BG_COL,BG_COL
 ''axes
 LINE AXIS_LEFT,AXIS_TOP,AXIS_RIGHT,AXIS_TOP,1,AXIS_COLOUR
 LINE AXIS_LEFT,AXIS_TOP,AXIS_LEFT,AXIS_BOTTOM,1,AXIS_COLOUR
 LINE AXIS_LEFT,AXIS_BOTTOM,AXIS_RIGHT,AXIS_BOTTOM,1,AXIS_COLOUR
 LINE AXIS_RIGHT,AXIS_TOP,AXIS_RIGHT,AXIS_BOTTOM,1,AXIS_COLOUR
 LINE AXIS_LEFT,(AXIS_TOP+AXIS_BOTTOM)\2,AXIS_RIGHT,(AXIS_TOP+AXIS_BOTTOM)\2,1,AXIS_COLOUR
 ''right labels
 TEXT AXIS_RIGHT+2,AXIS_TOP,STR$(V_FULL, 2, 1),LT,,1,V_COLOUR,BG_COL
 TEXT AXIS_RIGHT+2,AXIS_BOTTOM,STR$(V_EMPTY, 2, 1),LB,,1,V_COLOUR,BG_COL
 TEXT AXIS_RIGHT+2,(AXIS_TOP+AXIS_BOTTOM)\2,"V",LM,,1,V_COLOUR,BG_COL
 ''left labels
 TEXT AXIS_LEFT-1,AXIS_TOP,STR$(I_SCALE, 2,0),RT,,1,I2_COLOUR,BG_COL
 TEXT AXIS_LEFT-1,AXIS_BOTTOM,STR$(-I_SCALE, 2,0),RB,,1,I2_COLOUR,BG_COL
 TEXT AXIS_LEFT-1,(AXIS_TOP+AXIS_BOTTOM)\2-20,"I1",RM,,1,I1_COLOUR,BG_COL
 TEXT AXIS_LEFT-1,(AXIS_TOP+AXIS_BOTTOM)\2,"I2",RM,,1,I2_COLOUR,BG_COL
 TEXT AXIS_LEFT-1,(AXIS_TOP+AXIS_BOTTOM)\2+20,"I3",RM,,1,I3_COLOUR,BG_COL
 TEXT AXIS_LEFT-1,(AXIS_TOP+AXIS_BOTTOM)\2+40,"I4",RM,,1,I4_COLOUR,BG_COL
 ''bottom labels
 TEXT AXIS_RIGHT,AXIS_BOTTOM+2,"NOW",RT,,1,AXIS_COLOUR,BG_COL
 IF N = 0 THEN
   TEXT AXIS_LEFT,AXIS_BOTTOM+2,"-60 minutes",LT,,1,AXIS_COLOUR,BG_COL
   FOR I = 0 to S_COUNT-1
     ''heaps of data points, so just draw dots
     IF SAMPLES(1,((SAMPLE_STATE+I+1) MOD 720))>0 THEN 'valid data
       DRAW_SAMPLE_PIXELS(AXIS_LEFT+((AXIS_RIGHT-AXIS_LEFT)*I)\S_COUNT,((SAMPLE_STATE+I+1) MOD 720))
     END IF
   NEXT I
 ENDIF
 IF N = 1 THEN
   TEXT AXIS_LEFT,AXIS_BOTTOM+2,"-48 hours  ",LT,,1,AXIS_COLOUR,BG_COL
   DRAW_HOUR_LINES
 ENDIF
 IF N = 2 THEN
   TEXT AXIS_LEFT,AXIS_BOTTOM+2,STR$(-D_COUNT)+" days    ",LT,,1,AXIS_COLOUR,BG_COL
   DRAW_DAY_LINES
 ENDIF
END SUB
 
SUB DRAW_HOUR_LINES
 LOCAL INTEGER I,J
 LOCAL INTEGER X(H_COUNT),Y(6,H_COUNT)
 FOR I = 0 to H_COUNT-1
   ''calc y values
   IF H_SAMP(1,I) > 0 THEN
     Y(1,I)=AXIS_BOTTOM+(H_SAMP(1,I)-V_EMPTY)*(AXIS_BOTTOM-AXIS_TOP)/(V_EMPTY-V_FULL)
     IF Y(1,I) < AXIS_TOP+1 THEN Y(1,I) = AXIS_TOP+1
     IF Y(1,I) > AXIS_BOTTOM-1 THEN Y(1,I) = AXIS_BOTTOM-1
     FOR J = 2 to 5  ''current values
       Y(J,I)=(AXIS_BOTTOM+AXIS_TOP)\2 + H_SAMP(J,I)*(AXIS_BOTTOM-AXIS_TOP)/(2*I_SCALE)
       IF Y(J,I) < AXIS_TOP+1 THEN Y(J,I) = AXIS_TOP+1
       IF Y(J,I) > AXIS_BOTTOM-1 THEN Y(J,I) = AXIS_BOTTOM-1
     NEXT J
   ENDIF
   ''calc x values
   X(I)= AXIS_LEFT + ((I + H_COUNT - H_PTR)*(AXIS_RIGHT-AXIS_LEFT))/H_COUNT-1
   IF (X(I) < AXIS_LEFT) OR (X(I) > AXIS_RIGHT) THEN X(I)=0  ''not valid
 NEXT I
 ''draw lines
 FOR I = 0 to H_COUNT-2
   IF (X(I)>0) AND (X(I+1)>0) AND (Y(1,I)>0)AND (Y(1,I+1)>0) THEN
     LINE X(I),Y(1,I),X(I+1),Y(1,I+1),1,V_COLOUR
     LINE X(I),Y(2,I),X(I+1),Y(2,I+1),1,I1_COLOUR
     LINE X(I),Y(3,I),X(I+1),Y(3,I+1),1,I2_COLOUR
     LINE X(I),Y(4,I),X(I+1),Y(4,I+1),1,I3_COLOUR
     LINE X(I),Y(5,I),X(I+1),Y(5,I+1),1,I4_COLOUR
   ENDIF
 NEXT I
END SUB
 
SUB DRAW_DAY_LINES
 LOCAL INTEGER I,J
 LOCAL INTEGER X(D_COUNT),Y(6,D_COUNT)
 FOR I = 0 to D_COUNT-1
   ''calc y values
   IF D_SAMP(1,I) > 0 THEN
     Y(1,I)=AXIS_BOTTOM+(D_SAMP(1,I)-V_EMPTY)*(AXIS_BOTTOM-AXIS_TOP)/(V_EMPTY-V_FULL)
     IF Y(1,I) < AXIS_TOP+1 THEN Y(1,I) = AXIS_TOP+1
     IF Y(1,I) > AXIS_BOTTOM-1 THEN Y(1,I) = AXIS_BOTTOM-1
     FOR J = 2 to 5  ''current values
       Y(J,I)=(AXIS_BOTTOM+AXIS_TOP)\2 + D_SAMP(J,I)*(AXIS_BOTTOM-AXIS_TOP)/(2*I_SCALE)
       IF Y(J,I) < AXIS_TOP+1 THEN Y(J,I) = AXIS_TOP+1
       IF Y(J,I) > AXIS_BOTTOM-1 THEN Y(J,I) = AXIS_BOTTOM-1
     NEXT J
   ENDIF
   ''calc x values
   X(I)= AXIS_LEFT + ((I + D_COUNT - D_PTR)*(AXIS_RIGHT-AXIS_LEFT))/D_COUNT-1
   IF (X(I) < AXIS_LEFT) OR (X(I) > AXIS_RIGHT) THEN X(I)=0  ''not valid
 NEXT I
 ''draw lines
 FOR I = 0 to D_COUNT-2
   IF (X(I)>0) AND (X(I+1)>0) AND (Y(1,I)>0)AND (Y(1,I+1)>0) THEN
     LINE X(I),Y(1,I),X(I+1),Y(1,I+1),1,V_COLOUR
     LINE X(I),Y(2,I),X(I+1),Y(2,I+1),1,I1_COLOUR
     LINE X(I),Y(3,I),X(I+1),Y(3,I+1),1,I2_COLOUR
     LINE X(I),Y(4,I),X(I+1),Y(4,I+1),1,I3_COLOUR
     LINE X(I),Y(5,I),X(I+1),Y(5,I+1),1,I4_COLOUR
   ENDIF
 NEXT I
END SUB
 
SUB DRAW_SAMPLE_PIXELS(X AS INTEGER,N AS INTEGER) ''x coord, sample #
 LOCAL INTEGER Y
 ''data is validated already
'V:
 Y=AXIS_BOTTOM+(SAMPLES(1,N)-V_EMPTY)*(AXIS_BOTTOM-AXIS_TOP)/(V_EMPTY-V_FULL)
 IF Y < AXIS_TOP+1 THEN Y = AXIS_TOP+1
 IF Y > AXIS_BOTTOM-1 THEN Y = AXIS_BOTTOM-1
 PIXEL X,Y,V_COLOUR
''I1:
 Y=(AXIS_BOTTOM+AXIS_TOP)\2 + SAMPLES(2,N)*(AXIS_BOTTOM-AXIS_TOP)/(2*I_SCALE)
 IF Y < AXIS_TOP+1 THEN Y = AXIS_TOP+1
 IF Y > AXIS_BOTTOM-1 THEN Y = AXIS_BOTTOM-1
 PIXEL X,Y,I1_COLOUR
''I2:
 Y=(AXIS_BOTTOM+AXIS_TOP)\2 + SAMPLES(3,N)*(AXIS_BOTTOM-AXIS_TOP)/(2*I_SCALE)
 IF Y < AXIS_TOP+1 THEN Y = AXIS_TOP+1
 IF Y > AXIS_BOTTOM-1 THEN Y = AXIS_BOTTOM-1
 PIXEL X,Y,I2_COLOUR
''I3:
 Y=(AXIS_BOTTOM+AXIS_TOP)\2 + SAMPLES(4,N)*(AXIS_BOTTOM-AXIS_TOP)/(2*I_SCALE)
 IF Y < AXIS_TOP+1 THEN Y = AXIS_TOP+1
 IF Y > AXIS_BOTTOM-1 THEN Y = AXIS_BOTTOM-1
 PIXEL X,Y,I3_COLOUR
''I4:
 Y=(AXIS_BOTTOM+AXIS_TOP)\2 + SAMPLES(5,N)*(AXIS_BOTTOM-AXIS_TOP)/(2*I_SCALE)
 IF Y < AXIS_TOP+1 THEN Y = AXIS_TOP+1
 IF Y > AXIS_BOTTOM-1 THEN Y = AXIS_BOTTOM-1
 PIXEL X,Y,I4_COLOUR
END SUB
 
SUB EXPORT_DATA(N AS INTEGER)  ''0=hours, 1=days, 2=weeks
 IF N = 0 THEN EXPORT_HOURS
 IF N = 1 THEN EXPORT_DAYS
 IF N = 2 THEN EXPORT_WEEKS
END SUB
 
SUB EXPORT_HOURS
 LOCAL INTEGER I
 local INTEGER START_TIME
 START_TIME = (TIME_SERIAL()\5)*5-3600 '1 hour ago, round to 5s
 PRINT "Silicon Chip Battery Multi-Logger Hours Data Export"
 PRINT "Time,Voltage(V),Current 1 (A),Current 2 (A),Current 3 (A),Current 4 (A)"
 FOR I = 0 to S_COUNT-1
   IF SAMPLES(1,((SAMPLE_STATE+I+1) MOD 720))>0 THEN 'valid data
     PRINT DATE_TIME_STRING(START_TIME+I*5);",";
     PRINT STR$(SAMPLES(1,((SAMPLE_STATE+I+1) MOD 720)));",";
     PRINT STR$(SAMPLES(2,((SAMPLE_STATE+I+1) MOD 720)));",";
     PRINT STR$(SAMPLES(3,((SAMPLE_STATE+I+1) MOD 720)));",";
     PRINT STR$(SAMPLES(4,((SAMPLE_STATE+I+1) MOD 720)));",";
     PRINT STR$(SAMPLES(5,((SAMPLE_STATE+I+1) MOD 720)))
   ENDIF
 NEXT I
END SUB
 
SUB EXPORT_DAYS
 LOCAL INTEGER I
 local INTEGER START_TIME
 START_TIME = H_START*3600 'hour
 PRINT "Silicon Chip Battery Multi-Logger Days Data Export"
 PRINT "Time,Voltage(V),Current 1 (A),Current 2 (A),Current 3 (A),Current 4 (A)"
 FOR I = 0 to H_COUNT-1
   IF H_SAMP(1,I)>0 THEN 'valid data
     PRINT DATE_TIME_STRING(START_TIME+I*3600);",";
     PRINT STR$(H_SAMP(1,I));",";
     PRINT STR$(H_SAMP(2,I));",";
     PRINT STR$(H_SAMP(3,I));",";
     PRINT STR$(H_SAMP(4,I));",";
     PRINT STR$(H_SAMP(5,I))
   ENDIF
 NEXT I
END SUB
 
SUB EXPORT_WEEKS
 LOCAL INTEGER I
 local INTEGER START_TIME
 START_TIME = D_START*86400 'days
 PRINT "Silicon Chip Battery Multi-Logger Weeks Data Export"
 PRINT "Time,Voltage(V),Current 1 (A),Current 2 (A),Current 3 (A),Current 4 (A)"
 FOR I = 0 to D_COUNT-1
   IF D_SAMP(1,I)>0 THEN 'valid data
     PRINT DATE_TIME_STRING(START_TIME+I*86400);",";
     PRINT STR$(D_SAMP(1,I));",";
     PRINT STR$(D_SAMP(2,I));",";
     PRINT STR$(D_SAMP(3,I));",";
     PRINT STR$(D_SAMP(4,I));",";
     PRINT STR$(D_SAMP(5,I))
   ENDIF
 NEXT I
END SUB
 
FUNCTION DATE_TIME_STRING (T AS INTEGER) AS STRING
 'tweak this to suit your preferred display format
 'Excel format serial number
 DATE_TIME_STRING = STR$(T\86400,6,0)+"."+STR$((((T MOD 86400)*1000000)\86400),6,0,"0")
END FUNCTION
 
SUB DO_I_CAL(I_NUM AS INTEGER)
 LOCAL I_ENTERED AS FLOAT
 LOCAL NEW_CAL AS FLOAT
 LOCAL ACTUAL_NOW AS FLOAT  'temp as this may change
 ACTUAL_NOW=abs(ACTUAL(I_NUM+1))
 IF ABS(ACTUAL_NOW) < 0.00001 THEN MessageBox("I TOO LOW","Check and retry") : EXIT SUB
 CLS(BG_COL)
 IF RAW_ACTUAL(1) < 800000 THEN MessageBox("INPUT V TOO LOW","Check and retry") : EXIT SUB
 I_ENTERED=NUMBERPAD("Enter I(A)")
 IF I_ENTERED < 0.000001 THEN EXIT SUB   'cancel
 NEW_CAL = I_ENTERED/ACTUAL_NOW
 TEXT 160,10,"New I("+STR$(I_NUM)+") factor:",CT,8,1,FG_COL,BG_COL
 TEXT 160,40,"I factor:",RT,1,1,FG_COL,BG_COL
 TEXT 160,40,STR$(NEW_CAL),LT,1,1,FG_COL,BG_COL
 IF OK_CANCEL_BUTTONS() > 0 THEN
   ''accept and store values
   I_CAL(I_NUM)=-NEW_CAL
   VAR SAVE I_CAL()
 ENDIF
END SUB
 
SUB DO_V_CAL
 LOCAL V_ENTERED AS FLOAT
 LOCAL ACTUAL_NOW(5) AS FLOAT  'temp as these may change
 LOCAL NEW_CAL(5) AS FLOAT
 LOCAL NEW_MIN,NEW_MAX AS FLOAT
 CLS(BG_COL)
 ACTUAL_NOW(1)=RAW_ACTUAL(1) ''mapping
 ACTUAL_NOW(2)=RAW_ACTUAL(3)
 ACTUAL_NOW(3)=RAW_ACTUAL(4)
 ACTUAL_NOW(4)=RAW_ACTUAL(5)
 ''check if valid, this will also trap /0 error
 IF ACTUAL_NOW(1) < 800000 THEN MessageBox("INPUT V TOO LOW","Check and retry") : EXIT SUB
 IF ACTUAL_NOW(2) < 800000 THEN MessageBox("INPUT V TOO LOW","Check and retry") : EXIT SUB
 IF ACTUAL_NOW(3) < 800000 THEN MessageBox("INPUT V TOO LOW","Check and retry") : EXIT SUB
 IF ACTUAL_NOW(4) < 800000 THEN MessageBox("INPUT V TOO LOW","Check and retry") : EXIT SUB
 V_ENTERED=NUMBERPAD("Enter Voltage")
 IF V_ENTERED < 5.0 THEN MessageBox("ENTERED V TOO LOW","Check and retry") : EXIT SUB   'cancel
 NEW_CAL(1)=V_ENTERED/ACTUAL_NOW(1)
 ''old method
 'NEW_CAL(2)=V_ENTERED/ACTUAL_NOW(2)
 'NEW_CAL(3)=V_ENTERED/ACTUAL_NOW(3)
 'NEW_CAL(4)=V_ENTERED/ACTUAL_NOW(4)
 ''new method
 NEW_CAL(2)=NEW_CAL(1)*I_RATIO(2)
 NEW_CAL(3)=NEW_CAL(1)*I_RATIO(3)
 NEW_CAL(4)=NEW_CAL(1)*I_RATIO(4)
 NEW_MIN=NEW_CAL(1)
 IF NEW_CAL(2)<NEW_MIN THEN NEW_MIN=NEW_CAL(2)
 IF NEW_CAL(3)<NEW_MIN THEN NEW_MIN=NEW_CAL(3)
 IF NEW_CAL(4)<NEW_MIN THEN NEW_MIN=NEW_CAL(4)
 NEW_MAX=NEW_CAL(1)
 IF NEW_CAL(2)>NEW_MAX THEN NEW_MAX=NEW_CAL(2)
 IF NEW_CAL(3)>NEW_MAX THEN NEW_MAX=NEW_CAL(3)
 IF NEW_CAL(4)>NEW_MAX THEN NEW_MAX=NEW_CAL(4)
 CLS(BG_COL)
 TEXT 160,10,"New V factors:",CT,8,1,FG_COL,BG_COL
 TEXT 160,40,"V1 factor:",RT,1,1,FG_COL,BG_COL
 TEXT 160,60,"V2 factor:",RT,1,1,FG_COL,BG_COL
 TEXT 160,80,"V3 factor:",RT,1,1,FG_COL,BG_COL
 TEXT 160,100,"V4 factor:",RT,1,1,FG_COL,BG_COL
 TEXT 160,120,"Variation:",RT,1,1,FG_COL,BG_COL
 TEXT 160,40,STR$(NEW_CAL(1)),LT,1,1,FG_COL,BG_COL
 TEXT 160,60,STR$(NEW_CAL(2)),LT,1,1,FG_COL,BG_COL
 TEXT 160,80,STR$(NEW_CAL(3)),LT,1,1,FG_COL,BG_COL
 TEXT 160,100,STR$(NEW_CAL(4)),LT,1,1,FG_COL,BG_COL
 TEXT 160,120,STR$(((NEW_MAX-NEW_MIN)*100)/NEW_MAX)+"%",LT,1,1,FG_COL,BG_COL
 ''check before saving
 IF OK_CANCEL_BUTTONS() > 0 THEN
   ''accept and store values
   V_CAL(1)=NEW_CAL(1)
   V_CAL(2)=NEW_CAL(2)
   V_CAL(3)=NEW_CAL(3)
   V_CAL(4)=NEW_CAL(4)
   VAR SAVE V_CAL()
 ENDIF
END SUB
 
SUB DRAW_CALIBRATE
 CLS(BG_COL)
 TEXT 160,10,"CALIBRATE",CT,8,1,FG_COL,BG_COL
 TEXT 160,40,"V factor:",RT,1,1,FG_COL,BG_COL
 TEXT 160,60,"I1 factor:",RT,1,1,FG_COL,BG_COL
 TEXT 160,80,"I2 factor:",RT,1,1,FG_COL,BG_COL
 TEXT 160,100,"I3 factor:",RT,1,1,FG_COL,BG_COL
 TEXT 160,120,"I4 factor:",RT,1,1,FG_COL,BG_COL
 
 TEXT 160,40,STR$(V_CAL(1)),LT,1,1,FG_COL,BG_COL
 TEXT 160,60,STR$(I_CAL(1)),LT,1,1,FG_COL,BG_COL
 TEXT 160,80,STR$(I_CAL(2)),LT,1,1,FG_COL,BG_COL
 TEXT 160,100,STR$(I_CAL(3)),LT,1,1,FG_COL,BG_COL
 TEXT 160,120,STR$(I_CAL(4)),LT,1,1,FG_COL,BG_COL
 
 DrawButton 0, 0, 20, 140, 80, 40, BUTTON_COL, "Volts"
 DrawButton 1, 0, 120, 140, 80, 40, BUTTON_COL, "Current 1"
 DrawButton 2, 0, 220, 140, 80, 40, BUTTON_COL, "Current 2"
 DrawButton 3, 0, 20, 190, 80, 40, BUTTON_COL, "Current 3"
 DrawButton 4, 0, 120, 190, 80, 40, BUTTON_COL, "Current 4"
 DrawButton 5, 0, 220, 190, 80, 40, BUTTON_COL, "Back"
END SUB
 
SUB DRAW_SETTINGS
 CLS(BG_COL)
 TEXT 160,5,"SETTINGS",CT,8,1,FG_COL,BG_COL
 DrawButton 0, 0, 20, 45, 80, 25, BUTTON_COL, "Year"
 DrawButton 1, 0, 120, 45, 80, 25, BUTTON_COL, "Month"
 DrawButton 2, 0, 220, 45, 80, 25, BUTTON_COL, "Day"
 DrawButton 3, 0, 20, 75, 80, 25, BUTTON_COL, "Hour"
 DrawButton 4, 0, 120, 75, 80, 25, BUTTON_COL, "Minute"
 DrawButton 5, 0, 220, 75, 80, 25, BUTTON_COL, "Second"
 DrawButton 6, 0, 20, 105, 80, 25, BUTTON_COL, "B/L"
 DrawButton 7, 0, 120, 105, 80, 25, BUTTON_COL, "V(Full)"
 DrawButton 8, 0, 220, 105, 80, 25, BUTTON_COL, "Timeout"
 DrawButton 9, 0, 20, 150, 80, 25, BUTTON_COL, "B/L (dim)"
 DrawButton 10, 0, 120, 150, 80, 25, BUTTON_COL,"V(empty)"
 DrawButton 12, 0, 20, 195, 80, 25, BUTTON_COL, "I scale"  'these were added later
 DrawButton 13, 0, 120, 195, 80, 25, BUTTON_COL,"V(sdown)" 'these were added later
 DrawButton 11, 0, 220, 150, 80, 70, BUTTON_COL, "BACK"
 TEXT 60,132,STR$(ON_BL)+"%",CT,1,1,FG_COL,BG_COL
 TEXT 160,132,STR$(V_FULL,3,2)+"V",CT,1,1,FG_COL,BG_COL
 TEXT 260,132,STR$((MN_TMOUT+IDLE_TIMEOUT)\1000)+"s",CT,1,1,FG_COL,BG_COL
 
 TEXT 60,177,STR$(IDLE_BL)+"%",CT,1,1,FG_COL,BG_COL
 TEXT 160,177,STR$(V_EMPTY,3,2)+"V",CT,1,1,FG_COL,BG_COL
 
 TEXT 60,222,STR$(I_SCALE,4,1)+"A",CT,1,1,FG_COL,BG_COL
 TEXT 160,222,STR$(V_SHUTDOWN,3,2)+"V",CT,1,1,FG_COL,BG_COL
END SUB
 
SUB DRAW_DATA
 CLS(BG_COL)
 TEXT 160,10,"DATA",CT,8,1,FG_COL,BG_COL
 DrawButton 0, 0, 5, 210, 70, 24, BUTTON_COL, "Hours"
 DrawButton 1, 0, 85, 210, 70, 24, BUTTON_COL, "Days"
 DrawButton 2, 0, 165, 210, 70, 24, BUTTON_COL, "Weeks"
 DrawButton 3, 0, 245, 210, 70, 24, BUTTON_COL, "Exit"
 DrawButton 4, 0, 245, 6, 70, 24, BUTTON_COL, "Export"
END SUB
 
SUB DRAW_MAIN
 CLS(BG_COL)
 TEXT 160,10,"SILICON CHIP",CT,8,1,FG_COL,BG_COL
 TEXT 160,40,"BATTERY MULTI-LOGGER",CT,8,1,FG_COL,BG_COL
 DrawButton 0, 0, 20, 190, 80, 40, BUTTON_COL, "Data"
 DrawButton 1, 0, 120, 190, 80, 40, BUTTON_COL, "Settings"
 DrawButton 2, 0, 220, 190, 80, 40, BUTTON_COL, "Calibrate"
 DISPLAY_TIMEOUT=TIMER  ''reset on coming back to main
END SUB
 
FUNCTION SAMPLE_PATTERN(S AS INTEGER)
 ''map sample_state to channel
 SAMPLE_PATTERN=1
 IF S=2 OR S=3 THEN SAMPLE_PATTERN=2
 IF S=6 OR S=7 THEN SAMPLE_PATTERN=3
 IF S=10 OR S=11 THEN SAMPLE_PATTERN=4
END FUNCTION
 
SUB SAMPLE_MANAGER    ''can be called from anywhere to handle sample taking, updated 7 stage= V/I/V/I/V/I/V/I int
 STATIC INTEGER SAMPLE_STATE=0   ''state machine
 ''states are:
 '0=>1 = channel 1
 '2=>3 = channel 2
 '4=>5 = channel 1
 '6=>7 = channel 3
 '8=>9 = channel 1
 '10=>11 = channel 4
 '12=>13 = channel 1
 '14=process data
 STATIC INTEGER SAMPLE_TIMEOUT=0 ''to check for sampling timeouts
 STATIC INTEGER LAST_SAMPLE_STATE=0
 LOCAL FLOAT NEW_ACTUAL(6)
 SELECT CASE SAMPLE_STATE
     ''start samples
   CASE 0,2,4,6,8,10,12
     START_ADC(SAMPLE_PATTERN(SAMPLE_STATE))
     SAMPLE_TIMEOUT=TIMER
     SAMPLE_STATE=SAMPLE_STATE+1
     ''check if samples done/timeout
   CASE 1,3,5,7,9,11,13
     if CHECK_ADC_DONE(SAMPLE_PATTERN(SAMPLE_STATE)) > 0 THEN
       NEW_RAW((SAMPLE_STATE+1)\2)=GET_ADC_RESULT()
       SAMPLE_STATE=SAMPLE_STATE+1  ''avoid timeout
     ELSEIF (TIMER-SAMPLE_TIMEOUT)> ADC_TIMEOUT THEN
       NEW_RAW(1)=-1                ''flag timeout
       SAMPLE_STATE=SAMPLE_STATE+1  ''move on
     ENDIF
     ''process data
   CASE 14
     NEW_RAW(8)=PIN(SHUNT_ADC)  'this is in volts
     IF  NEW_RAW(1)<0 THEN
       'PRINT "SAMPLE CYCLE ERROR"
     ELSE
       RAW_ACTUAL(1)=(NEW_RAW(1)+NEW_RAW(3)+NEW_RAW(5)+NEW_RAW(7))\4  ''this is used as a flag
       RAW_ACTUAL(3)=NEW_RAW(2)  ''this is used as a flag
       RAW_ACTUAL(4)=NEW_RAW(4)  ''this is used as a flag
       RAW_ACTUAL(5)=NEW_RAW(6)  ''this is used as a flag
       ''values for calibration
       IF NEW_RAW(2)>800000 THEN I_RATIO(2)=(NEW_RAW(1)+NEW_RAW(3))/(NEW_RAW(2)*2) ELSE I_RATIO(2)=1
       IF NEW_RAW(4)>800000 THEN I_RATIO(3)=(NEW_RAW(3)+NEW_RAW(5))/(NEW_RAW(4)*2) ELSE I_RATIO(3)=1
       IF NEW_RAW(6)>800000 THEN I_RATIO(4)=(NEW_RAW(5)+NEW_RAW(7))/(NEW_RAW(6)*2) ELSE I_RATIO(4)=1
       ''debugging
       'PRINT I_RATIO(2),"  ",I_RATIO(3),"   ",I_RATIO(4)
       ''actual calcs
       NEW_ACTUAL(1)=(NEW_RAW(1)+NEW_RAW(3)+NEW_RAW(5)+NEW_RAW(7))*V_CAL(1)/4
       NEW_ACTUAL(2)=NEW_RAW(8)*I_CAL(1)  ''this is a simple shunt
       NEW_ACTUAL(3)=(((NEW_RAW(1)+NEW_RAW(3))*V_CAL(1)/2)-(NEW_RAW(2))*V_CAL(2))*I_CAL(2)  ''differential voltage
       NEW_ACTUAL(4)=(((NEW_RAW(3)+NEW_RAW(5))*V_CAL(1)/2)-(NEW_RAW(4))*V_CAL(3))*I_CAL(3)  ''differential voltage
       NEW_ACTUAL(5)=(((NEW_RAW(5)+NEW_RAW(7))*V_CAL(1)/2)-(NEW_RAW(6))*V_CAL(4))*I_CAL(4)  ''differential voltage
       'NEW_ACTUAL(3)=((NEW_RAW(1)+NEW_RAW(3))\2-NEW_RAW(2))*V_CAL(2)*I_CAL(2) ''these are differential voltages
       'NEW_ACTUAL(4)=((NEW_RAW(3)+NEW_RAW(5))\2-NEW_RAW(4))*V_CAL(3)*I_CAL(3) ''these are differential voltages
       'NEW_ACTUAL(5)=((NEW_RAW(5)+NEW_RAW(7))\2-NEW_RAW(6))*V_CAL(4)*I_CAL(4) ''these are differential voltages
       ''unsmoothed
       ACTUAL(1)=NEW_ACTUAL(1)
       ACTUAL(2)=NEW_ACTUAL(2)
       ACTUAL(3)=NEW_ACTUAL(3)
       ACTUAL(4)=NEW_ACTUAL(4)
       ACTUAL(5)=NEW_ACTUAL(5)
       ''smoothed
       'ACTUAL(1)=(ACTUAL(1)*SMOOTHING_FACTOR+NEW_ACTUAL(1))/(SMOOTHING_FACTOR+1)
       'ACTUAL(2)=(ACTUAL(2)*SMOOTHING_FACTOR+NEW_ACTUAL(2))/(SMOOTHING_FACTOR+1)
       'ACTUAL(3)=(ACTUAL(3)*SMOOTHING_FACTOR+NEW_ACTUAL(3))/(SMOOTHING_FACTOR+1)
       'ACTUAL(4)=(ACTUAL(4)*SMOOTHING_FACTOR+NEW_ACTUAL(4))/(SMOOTHING_FACTOR+1)
       'ACTUAL(5)=(ACTUAL(5)*SMOOTHING_FACTOR+NEW_ACTUAL(5))/(SMOOTHING_FACTOR+1)
     ENDIF
     ''finish off the same as state 0
     NEW_RAW(1)=0'reset flag
     START_ADC(1)
     SAMPLE_TIMEOUT=TIMER
     SAMPLE_STATE=1
     DATA_MANAGER    ''save and store data
 END SELECT
''debugging:
 'IF LAST_SAMPLE_STATE <> SAMPLE_STATE THEN PRINT "SAMPLE:";LAST_SAMPLE_STATE;">";SAMPLE_STATE;"(";TIMER-SAMPLE_TIMEOUT;")": LAST_SAMPLE_STATE=SAMPLE_STATE
END SUB
 
SUB DATA_MANAGER
 LOCAL INTEGER S_PERIODS '' to adjust AH/WH
 LOCAL FLOAT AH_NOW, WH_NOW
 LOCAL INTEGER S_NUM
 LOCAL INTEGER I,J,NEW_H_PTR
 LOCAL FLOAT DATA_BUFFER(6)
 LOCAL INTEGER DATA_COUNT  ' if not all valid
 S_NUM=(TIME_SERIAL()\5) MOD 720 ''5 second periods
 'PRINT S_NUM;":";TIME_SERIAL()/86400;":";TIME_SERIAL()
 'update AH/WH usage
 S_PERIODS=(S_NUM + 720 - SAMPLE_STATE) MOD 720  'this takes care of occasional missed samples
 IF RAW_ACTUAL(1)>=0 THEN   'data is valid
   AH_NOW = (ACTUAL(2)+ACTUAL(3)+ACTUAL(4)+ACTUAL(5)) * S_PERIODS / 720  'sum of currents times interval in hours
   AH_SINCEF = AH_SINCEF + AH_NOW
   AH_SINCEE = AH_SINCEE + AH_NOW
   WH_NOW = AH_NOW * ACTUAL(1)
   WH_SINCEF = WH_SINCEF + WH_NOW
   WH_SINCEE = WH_SINCEE + WH_NOW
   IF ACTUAL(1) < V_EMPTY THEN
     AH_SINCEE=0
     WH_SINCEE=0
     USAGE_STATE = USAGE_STATE OR &H1  'has reached empty state
   ENDIF
   IF ACTUAL(1) > V_FULL THEN
     AH_SINCEF=0
     WH_SINCEF=0
     USAGE_STATE = USAGE_STATE OR &H2  'has reached full state
   ENDIF
 ENDIF
 IF SAMPLE_STATE < S_NUM THEN  'new sample in current hour
   'PRINT "SAMPLE:";S_NUM;":";SAMPLE_STATE
   ''need to clean up old samples from S_NUM to SAMPLE_STATE, but check for off by one.
   ''SAMPLE_STATE+1 to S_NUM (which should usually be the same, current sample about to be written)
   FOR J= SAMPLE_STATE+1 TO S_NUM
     SAMPLES(1,J)=-1'clear and flag
   NEXT J
   'PRINT "CLEARED:";SAMPLE_STATE+1;" to ";S_NUM
   FOR I=1 TO 5
     SAMPLES(I,S_NUM)=ACTUAL(I)    ''store
   NEXT I
   SAMPLE_STATE=S_NUM              ''update state
 ENDIF
 IF S_NUM < SAMPLE_STATE THEN    'hour expired or other rollover
   SAMPLE_STATE=0  ''reset
   'PRINT "HOUR EXPIRED"
   FOR I = 1 to 5
     DATA_BUFFER(I)=0
   NEXT I
   DATA_COUNT=0
   FOR J = 1 TO 720
     IF SAMPLES(1,J)>0 THEN      ''valid data
       FOR I = 1 TO 5
         DATA_BUFFER(I)=DATA_BUFFER(I)+SAMPLES(I,J)
       NEXT I
       DATA_COUNT=DATA_COUNT+1
     ENDIF
   NExt J
   IF DATA_COUNT>0 THEN    ''valid data in last hour
     'PRINT "DATA IN LAST HOUR:";DATA_COUNT
     IF H_START=0 THEN     ''prepare hour array and shift out if necessary
       H_START=TIME_SERIAL()\3600    ''hour number
       H_PTR=0                       ''first valid data
     ELSE
       NEW_H_PTR=(TIME_SERIAL()\3600)-H_START
       'IF NEW_H_PTR <= H_PTR THEN PRINT "TIME ERROR:";NEW_H_PTR
       IF NEW_H_PTR >= H_COUNT THEN NEW_H_PTR=ARCHIVE_HOUR_DATA(NEW_H_PTR) ''need to shift out old hourly data
       H_PTR=NEW_H_PTR''may need refining
     ENDIF
     FOR I = 1 TO 5
       H_SAMP(I,H_PTR)=DATA_BUFFER(I)/DATA_COUNT
     NEXT I
     VAR SAVE H_PTR,H_START,H_SAMP()
     'PRINT "Saved an hour of samples:";H_PTR
     'H_PTR=H_PTR+1  ''calculated automatically
   ELSE
     'PRINT "NO VALID DATA IN LAST HOUR"
   ENDIF
   IF (((TIME_SERIAL()\3600) MOD 24) = 0) THEN DAY_DATA_MANAGER  ''handle saving days of data in first hour after midnight
 ENDIF
 'ENDIF  '?
END SUB
 
SUB DAY_DATA_MANAGER  ''do what needs to be done for daily archiving, same as hourly, but different timescales
 LOCAL INTEGER I,J,NEW_D_PTR
 LOCAL FLOAT DATA_BUFFER(6)
 LOCAL INTEGER DATA_COUNT  ' if not all valid
 LOCAL INTEGER H_TO_START, H_TO_END
 H_TO_START  = (TIME_SERIAL()\3600)-24-H_START ''index of start of yesterday
 H_TO_END = H_TO_START+23    ''24 in a day
 if H_TO_START < 0 THEN H_TO_START = 0
 IF H_TO_END > H_COUNT-1 THEN H_TO_END = H_COUNT-1
 IF H_TO_END < H_TO_START THEN
   'PRINT "H_TO_STORE INVALID:";H_TO_START;"-";H_TO_END
   EXIT SUB
 ENDIF
 FOR I = 1 to 5
   DATA_BUFFER(I)=0
 NEXT I
 DATA_COUNT=0
 FOR J = H_TO_START TO H_TO_END
   IF H_SAMP(1,J)>0 THEN      ''valid data
     FOR I = 1 TO 5
       DATA_BUFFER(I)=DATA_BUFFER(I)+H_SAMP(I,J)
     NEXT I
     DATA_COUNT=DATA_COUNT+1
   ENDIF
 NExt J
 'PRINT "DATA IN LAST DAY:";DATA_COUNT;"=";H_TO_START;"-";H_TO_END
 IF DATA_COUNT>0 THEN    ''valid data in last hour
   IF D_START=0 THEN     ''prepare hour array and shift out if necessary
     D_START=TIME_SERIAL()\86400    ''day number
     D_PTR=0                       ''first valid data
   ELSE
     NEW_D_PTR=(TIME_SERIAL()\86400)-D_START
     'IF NEW_D_PTR <= D_PTR THEN PRINT "TIME ERROR:";NEW_D_PTR
     IF NEW_D_PTR >= D_COUNT THEN NEW_D_PTR=ARCHIVE_DAY_DATA(NEW_D_PTR) ''need to shift out old hourly data
     D_PTR=NEW_D_PTR''may need refining
   ENDIF
   FOR I = 1 TO 5
     D_SAMP(I,D_PTR)=DATA_BUFFER(I)/DATA_COUNT
   NEXT I
   VAR SAVE D_PTR,D_START,D_SAMP()
   'PRINT "Saved a day of samples:";D_PTR
 ELSE
   'PRINT "NO VALID DATA IN LAST DAY"
 ENDIF
END SUB
 
FUNCTION ARCHIVE_DAY_DATA(D AS INTEGER) AS INTEGER
 ARCHIVE_DAY_DATA = D
 if D < D_COUNT THEN EXIT FUNCTION ''don't do anything if there's room left
 LOCAL INTEGER OFFSET,I,J
 OFFSET = D - D_COUNT + 1 ''should be 1 most of the time
 FOR J = 0 TO D_COUNT - OFFSET - 1
   FOR I = 1 TO 5
     D_SAMP(I,J)=D_SAMP(I,J+OFFSET)    ''shift by offset
   NEXT I
 NEXT J
 D_START=D_START+OFFSET                      ''adjust sample pointer
 ARCHIVE_DAY_DATA=D_COUNT-1   ''should be 47 normally
 'PRINT "Day data shifted by ";OFFSET
END FUNCTION
 
 
FUNCTION ARCHIVE_HOUR_DATA(H AS INTEGER) AS INTEGER
 ARCHIVE_HOUR_DATA = H
 if H < H_COUNT THEN EXIT FUNCTION ''don't do anything if there's room left
 LOCAL INTEGER OFFSET,I,J
 OFFSET = H - H_COUNT + 1 ''should be 1 most of the time
 FOR J = 0 TO H_COUNT - OFFSET - 1
   FOR I = 1 TO 5
     H_SAMP(I,J)=H_SAMP(I,J+OFFSET)    ''shift by offset
   NEXT I
 NEXT J
 H_START=H_START+OFFSET                      ''adjust sample pointer
 ''VAR SAVE H_START,H_SAMP()              ''will be done later
 ARCHIVE_HOUR_DATA=H_COUNT-1   ''should be 47 normally
 'PRINT "Hour data shifted by ";OFFSET
END FUNCTION
 
FUNCTION TIME_SERIAL() AS INTEGER   ''convert current date/time into a time serial number, number of seconds since 1/1/1900 (time serial number * 86400)
 LOCAL INTEGER T(6)
 T(0)=val(mid$(DATE$,7,4)) 'year
 T(1)=val(mid$(DATE$,4,2)) 'month
 T(2)=val(mid$(DATE$,1,2)) 'day
 T(3)=val(mid$(TIME$,1,2)) 'hour/24
 T(4)=val(mid$(TIME$,4,2)) 'minute
 T(5)=val(mid$(TIME$,7,2)) 'second
 TIME_SERIAL=TIME_SERIAL+T(2)+1  'from start of current month
 IF T(1)=12 THEN TIME_SERIAL=TIME_SERIAL+IS_LEAP_YEAR(T(0))+334
 IF T(1)=11 THEN TIME_SERIAL=TIME_SERIAL+IS_LEAP_YEAR(T(0))+304
 IF T(1)=10 THEN TIME_SERIAL=TIME_SERIAL+IS_LEAP_YEAR(T(0))+273
 IF T(1)=9 THEN TIME_SERIAL=TIME_SERIAL+IS_LEAP_YEAR(T(0))+243
 IF T(1)=8 THEN TIME_SERIAL=TIME_SERIAL+IS_LEAP_YEAR(T(0))+212
 IF T(1)=7 THEN TIME_SERIAL=TIME_SERIAL+IS_LEAP_YEAR(T(0))+181
 IF T(1)=6 THEN TIME_SERIAL=TIME_SERIAL+IS_LEAP_YEAR(T(0))+151
 IF T(1)=5 THEN TIME_SERIAL=TIME_SERIAL+IS_LEAP_YEAR(T(0))+120
 IF T(1)=4 THEN TIME_SERIAL=TIME_SERIAL+IS_LEAP_YEAR(T(0))+90
 IF T(1)=3 THEN TIME_SERIAL=TIME_SERIAL+IS_LEAP_YEAR(T(0))+59
 IF T(1)=2 THEN TIME_SERIAL=TIME_SERIAL+31
 'January is already start of year
 DO WHILE(T(0)>1900)
   T(0)=T(0)-1
   TIME_SERIAL=TIME_SERIAL+365+IS_LEAP_YEAR(T(0))
 LOOP
 TIME_SERIAL=TIME_SERIAL*86400+T(3)*3600+T(4)*60+T(5)
END FUNCTION
 
FUNCTION IS_LEAP_YEAR(Y AS INTEGER) AS INTEGER
 IS_LEAP_YEAR=0
 IF ((Y\4)*4)=Y THEN IS_LEAP_YEAR =1
 IF ((Y\100)*100)=Y THEN IS_LEAP_YEAR =0
 IF ((Y\400)*400)=Y THEN IS_LEAP_YEAR =1
END FUNCTION
 
SUB WAIT_FOR_RELEASE
 DO WHILE (CHECK_SCREEN_TOUCH()=1)
   PAUSE 100
 LOOP
END SUB
 
FUNCTION CHECK_SCREEN_TOUCH() AS INTEGER
 LOCAL PIN_10_SAVE AS INTEGER
 PIN_10_SAVE=PIN(10)     ''save state
 pin(10)=1             ''power on so that touch pin is not pulled down
 CHECK_SCREEN_TOUCH=0
 IF (PIN(15)=0) THEN CHECK_SCREEN_TOUCH=1
 pin(10)=PIN_10_SAVE     'restore
END FUNCTION
 
SUB SET_BACKLIGHT(B AS INTEGER)
 PWM 2,50000,B
 'PRINT "SET_BACKLIGHT:";B
END SUB
 
Sub LCD_ON
 SetPin 10,DOUT
 pin(10)=1
 GUI RESET LCDPANEL        ''redo init, this should reinstate all control lines
End Sub
 
Sub LCD_OFF
 'PRINT "LCD OFF"
 Poke word &hBF886034,9      ''force control lines off
 Poke word &hBF886134,36876
 'Poke word &hBF886134,36868
End Sub
 
SUB START_ADC(C AS INTEGER)   ''start conversion on channel c
 LOCAL INTEGER CHANNEL_MASK,JUNK,TMOUT
 CHANNEL_MASK=0    ''no channels
 'LOCAL PIN_10_SAVE AS INTEGER
 'PIN_10_SAVE=PIN(10)     ''save state
 'pin(10)=1             ''power on so that touch pin is not pulled down
 SPI OPEN 2000000, 3, 8
 if C = 1 THEN CHANNEL_MASK=&h10
 if C = 2 THEN CHANNEL_MASK=&h20
 if C = 3 THEN CHANNEL_MASK=&h40
 if C = 4 THEN CHANNEL_MASK=&h80
 pin(ADC_CS)=0
 ''40 DIN high bits=reset
 JUNK=SPI(255)
 JUNK=SPI(255)
 JUNK=SPI(255)
 JUNK=SPI(255)
 JUNK=SPI(255)
 pin(ADC_CS)=1
 pin(ADC_CS)=0
 JUNK=SPI(8)   ''write MODE, 24 bits
 JUNK=SPI(&h38) ''single mode, returns to pwoer down after
 JUNK=SPI(&h07) ''sinc4, no parity, FS9-FS8=3
 JUNK=SPI(&hFF) ''FS7-FS0=FF
 pin(ADC_CS)=1
 pin(ADC_CS)=0
 JUNK=SPI(16)   ''write CONFIG, 24 bits
 JUNK=SPI(&H00) ''CHOP off
 JUNK=SPI(CHANNEL_MASK)
 JUNK=SPI(&H48) ''unipolar, gain=1,
 pin(ADC_CS)=1
 SPI CLOSE
 'pin(10)=PIN_10_SAVE     'restore
END SUB
 
FUNCTION CHECK_ADC_DONE(C AS INTEGER) AS INTEGER
 LOCAL INTEGER JUNK
 CHECK_ADC_DONE=0
 'LOCAL PIN_10_SAVE AS INTEGER
 'PIN_10_SAVE=PIN(10)     ''save state
 'pin(10)=1             ''power on so that touch pin is not pulled down
 SPI OPEN 2000000, 3, 8
 pin(ADC_CS)=0
 JUNK=SPI(64)  ''read status
 JUNK=SPI(0)
 pin(ADC_CS)=1
 if (JUNK AND 7) = (C+3) THEN CHECK_ADC_DONE=1
 SPI CLOSE
 'pin(10)=PIN_10_SAVE     'restore
END FUNCTION
 
FUNCTION GET_ADC_RESULT() AS INTEGER
 LOCAL INTEGER JUNK
 GET_ADC_RESULT=-1
 'LOCAL PIN_10_SAVE AS INTEGER
 'PIN_10_SAVE=PIN(10)     ''save state
 'pin(10)=1             ''power on so that touch pin is not pulled down
 SPI OPEN 2000000, 3, 8
 pin(ADC_CS)=0
 JUNK=SPI(88)  ''read data
 GET_ADC_RESULT=SPI(0)*65536        ''load valid data
 GET_ADC_RESULT=GET_ADC_RESULT + SPI(0)*256
 GET_ADC_RESULT=GET_ADC_RESULT + SPI(0)
 pin(ADC_CS)=1
 'print GET_ADC_RESULT
 SPI CLOSE
 'pin(10)=PIN_10_SAVE     'restore
END FUNCTION
 
FUNCTION GET_ADC(C AS INTEGER) AS FLOAT   ''blocking conversion
 GET_ADC=-1'default fail value
 LOCAL INTEGER CHANNEL_MASK,JUNK,TMOUT
 TMOUT=2000
 CHANNEL_MASK=0    ''no channels
 'LOCAL PIN_10_SAVE AS INTEGER
 'PIN_10_SAVE=PIN(10)     ''save state
 'pin(10)=1             ''power on so that touch pin is not pulled down
 SPI OPEN 2000000, 3, 8
 if C = 1 THEN CHANNEL_MASK=&h10
 if C = 2 THEN CHANNEL_MASK=&h20
 if C = 3 THEN CHANNEL_MASK=&h40
 if C = 4 THEN CHANNEL_MASK=&h80
 pin(ADC_CS)=0
 ''40 DIN high bits=reset
 JUNK=SPI(255)
 JUNK=SPI(255)
 JUNK=SPI(255)
 JUNK=SPI(255)
 JUNK=SPI(255)
 pin(ADC_CS)=1
 pin(ADC_CS)=0
 JUNK=SPI(8)   ''write MODE, 24 bits
 JUNK=SPI(&h38) ''single mode, returns to pwoer down after
 JUNK=SPI(&h07) ''sinc4, no parity, FS9-FS8=3
 JUNK=SPI(&hFF) ''FS7-FS0=FF
 pin(ADC_CS)=1
 pin(ADC_CS)=0
 JUNK=SPI(16)   ''write CONFIG, 24 bits
 JUNK=SPI(&H00) ''CHOP off
 JUNK=SPI(CHANNEL_MASK)
 JUNK=SPI(&H48) ''unipolar, gain=1,
 pin(ADC_CS)=1
 DO WHILE(TMOUT>0)
   pin(ADC_CS)=0
   JUNK=SPI(64)  ''read status
   JUNK=SPI(0)
   pin(ADC_CS)=1
   'PRINT JUNK
   if (JUNK AND 7) = (C+3) THEN
     TMOUT=-1
     pin(ADC_CS)=0
     JUNK=SPI(88)  ''read data
     GET_ADC=SPI(0)*65536        ''load valid data
     GET_ADC=GET_ADC + SPI(0)*256
     GET_ADC=GET_ADC + SPI(0)
     pin(ADC_CS)=1
   END IF
   PAUSE 100
   TMOUT=TMOUT-100
 LOOP
 'Print "DATA "
 'pin(ADC_CS)=0
 'print hex$(SPI(88))   ''DATA, 24 bits
 'print hex$(SPI(0))
 'print hex$(SPI(0))
 'print hex$(SPI(0))
 'pin(ADC_CS)=1
 SPI CLOSE
 'pin(10)=PIN_10_SAVE     'restore
END FUNCTION
 
 
FUNCTION GET_ADC_FAST(C AS INTEGER) AS FLOAT   ''uses lower filter word for faster conversion
 GET_ADC_FAST=-1000000'default fail value
 LOCAL INTEGER CHANNEL_MASK,JUNK,TMOUT
 TMOUT=200
 CHANNEL_MASK=0    ''no channels
 'LOCAL PIN_10_SAVE AS INTEGER
 'PIN_10_SAVE=PIN(10)     ''save state
 'pin(10)=1             ''power on so that touch pin is not pulled down
 SPI OPEN 2000000, 3, 8
 if C = 1 THEN CHANNEL_MASK=&h10
 if C = 2 THEN CHANNEL_MASK=&h20
 if C = 3 THEN CHANNEL_MASK=&h40
 if C = 4 THEN CHANNEL_MASK=&h80
 pin(ADC_CS)=0
 ''40 DIN high bits=reset
 JUNK=SPI(255)
 JUNK=SPI(255)
 JUNK=SPI(255)
 JUNK=SPI(255)
 JUNK=SPI(255)
 pin(ADC_CS)=1
 pin(ADC_CS)=0
 JUNK=SPI(8)   ''write MODE, 24 bits
 JUNK=SPI(&h38) ''single mode, returns to pwoer down after
 JUNK=SPI(&h00) ''sinc4, no parity, FS9-FS8=0
 JUNK=SPI(32) ''FS7-FS0=value=filter word if FS9:FS8=0
 pin(ADC_CS)=1
 pin(ADC_CS)=0
 JUNK=SPI(16)   ''write CONFIG, 24 bits
 JUNK=SPI(&H00) ''CHOP off
 JUNK=SPI(CHANNEL_MASK)
 JUNK=SPI(&H48) ''unipolar, gain=1,
 pin(ADC_CS)=1
 DO WHILE(TMOUT>0)
   pin(ADC_CS)=0
   JUNK=SPI(64)  ''read status
   JUNK=SPI(0)
   pin(ADC_CS)=1
   'PRINT JUNK
   if (JUNK AND 7) = (C+3) THEN
     TMOUT=-1
     pin(ADC_CS)=0
     JUNK=SPI(88)  ''read data
     GET_ADC_FAST=SPI(0)*65536        ''load valid data
     GET_ADC_FAST=GET_ADC_FAST + SPI(0)*256
     GET_ADC_FAST=GET_ADC_FAST + SPI(0)
     pin(ADC_CS)=1
   END IF
   PAUSE 10
   TMOUT=TMOUT-10
 LOOP
 'Print "DATA "
 'pin(ADC_CS)=0
 'print hex$(SPI(88))   ''DATA, 24 bits
 'print hex$(SPI(0))
 'print hex$(SPI(0))
 'print hex$(SPI(0))
 'pin(ADC_CS)=1
 SPI CLOSE
 'pin(10)=PIN_10_SAVE     'restore
END FUNCTION
 
 
 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
 ' Draw buttons and get button presses
 '
 ' The subrouting DrawButton will draw a button (normally used when drawing
 ' the screen for input).
 '
 ' The function CheckButtonPress() will check if a button has been touched.
 ' If it has it will set it to selected (reverse video) and return with the
 ' button's number.
 '
 ' The subroutine CheckButtonRelease will wait for the touch to be released
 ' and will then draw the button as normal.
 '
 ' These routines use the global arrays key_coord() and key_caption() to
 ' track the coordinates and size of each button and save its caption.
 '
 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
 
 ' draw a button
Sub DrawButton n As Integer, mode As Integer, x As Integer, y As Integer, w As Integer, h As Integer, c As Integer, s As String
 Local Integer bc, fc
 
 If mode = 0 Then
   key_coord(n,0) = x : key_coord(n,1) = y : key_coord(n,2) = w : key_coord(n,3) = h
   key_coord(n,4) = c : key_caption(n) = s
 EndIf
 
 If mode > 1 Then
   bc = key_coord(n,4) : fc = 0    ' draw in reverse video if it is being touched
 Else
   bc = 0 : fc = key_coord(n,4)    ' a normal (untouched) button
 EndIf
 
 RBox key_coord(n,0), key_coord(n,1), key_coord(n,2), key_coord(n,3), , key_coord(n,4), bc)
 Text key_coord(n,0) + key_coord(n,2)/2, key_coord(n,1) + key_coord(n,3)/2, key_caption(n), CM, , , fc, bc
End Sub
 
Function CheckButtonPress2(startn As Integer, endn As Integer) As Integer
 CheckButtonPress = -1
 ''dummy empty
End Function
 
 ' check if a button has been touch and animate the button's image
 ' returns the button's number
Function CheckButtonPress(startn As Integer, endn As Integer) As Integer
 Local Integer xt, yellowt, n
 
 CheckButtonPress = -1
 If Touch(x) <> -1 Then
   ' we have a touch
   'WatchDog 1200000
   xt = Touch(x)
   yellowt = Touch(y)
   ' scan the array key_coord() to see if the touch was within the
   ' boundaries of a button
   For n = startn To endn
     If xt > key_coord(n,0) And xt < key_coord(n,0) + key_coord(n,2) And yellowt > key_coord(n,1) And yellowt < key_coord(n,1) + key_coord(n,3) Then
       ' we have a button press
       ' draw the button as pressed
       DrawButton n, 2
       CheckButtonPress = n
       Exit For
     EndIf
   Next n
 EndIf
End Function
 
 
 ' wait for the touch to be released and then draw the button as normal
Sub CheckButtonRelease n As Integer
 ' if a button is currently down check if it has been released
 Do While Touch(x) <> -1
   SAMPLE_MANAGER
 Loop   ' wait for the button to be released
 DrawButton n, 1                  ' draw the button as normal (ie, not pressed)
End Sub
 
 
 
 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
 ' this handy routine draws a message box with an OK button
 ' then waits for the button to be touched
 '''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''''
Sub MessageBox s1 As String, s2 As String
 Local Integer w
 If Len(s1) > Len(s2) Then w = Len(s1) Else w = Len(s2)
 w = w * 8     ' get the width of the text (used for the box width)
 
 ' draw the box and the message in it
 RBox MM.HRes/2 - w - 20, 60, w * 2 + 40, 130, , FG_COL, 0
 Text MM.HRes/2, 70, s1, CT, 1, 2, FG_COL
 Text MM.HRes/2, 100, s2, CT, 1, 2, FG_COL
 
 ' draw the OK button
 RBox 110, 140, 100, 34, , BUTTON_COL
 Text MM.HRes/2, 157, "OK", CM, 1, 2, BUTTON_COL
 
 ' wait for the button to be touched
 'WatchDog 1200000
 Do While Not (Touch(x) > 110 And Touch(x) < 210 And Touch(y) > 140 And Touch(y) < 180) : Loop
 
 ' draw the OK button as depressed
 RBox 110, 140, 100, 34, , BUTTON_COL, BUTTON_COL
 Text MM.HRes/2, 157, "OK", CM, 1, 2, 0, BUTTON_COL
 
 ' wait for the touch to be removed
 Do While Touch(x) <> -1 : Loop
End Sub
 
FUNCTION OK_CANCEL_BOX(S1 AS STRING, S2 AS STRING) AS INTEGER   ''variant of MessageBox with two options
 OK_CANCEL_BOX=0
 Local Integer w
 If Len(s1) > Len(s2) Then w = Len(s1) Else w = Len(s2)
 w = w * 8     ' get the width of the text (used for the box width)
 if w < 108 THEN w = 108
 
 ' draw the box and the message in it
 RBox MM.HRes/2 - w - 20, 60, w * 2 + 40, 130, , FG_COL, 0
 Text MM.HRes/2, 70, s1, CT, 1, 2, FG_COL
 Text MM.HRes/2, 100, s2, CT, 1, 2, FG_COL
 
 ' draw the OK button
 RBox 55, 140, 100, 34, , BUTTON_COL
 Text 105, 157, "OK", CM, 1, 2, BUTTON_COL
 ' draw the CANCEL button
 RBox 165, 140, 100, 34, , BUTTON_COL
 Text 215, 157, "CANCEL", CM, 1, 2, BUTTON_COL
 'wait for press
 DO
   SAMPLE_MANAGER
   IF  (Touch(y) > 140 And Touch(y) < 180) THEN
     IF (TOUCH(x)>55 AND TOUCH(x)<155) THEN  ''OK
       RBox 55, 140, 100, 34, , BUTTON_COL, BUTTON_COL
       Text 105, 157, "OK", CM, 1, 2, 0, BUTTON_COL
       Do While Touch(x) <> -1 : SAMPLE_MANAGER : Loop
       OK_CANCEL_BOX=1
       EXIT FUNCTION
     ENDIF
     IF (TOUCH(x)>165 AND TOUCH(x)<265) THEN  ''CANCEL
       RBox 165, 140, 100, 34, , BUTTON_COL, BUTTON_COL
       Text 215, 157, "CANCEL", CM, 1, 2, 0, BUTTON_COL
       Do While Touch(x) <> -1 : SAMPLE_MANAGER : Loop
       OK_CANCEL_BOX=0
       EXIT FUNCTION
     ENDIF
   ENDIF
 LOOP
 
END FUNCTION
 
FUNCTION OK_CANCEL_BUTTONS() AS INTEGER   ''buttons only at bottom of screen
 OK_CANCEL_BUTTONS=0
 ' draw the OK button
 RBox 55, 200, 100, 34, , BUTTON_COL
 Text 105, 217, "OK", CM, 1, 2, BUTTON_COL
 ' draw the CANCEL button
 RBox 165, 200, 100, 34, , BUTTON_COL
 Text 215, 217, "CANCEL", CM, 1, 2, BUTTON_COL
 'wait for press
 DO
   SAMPLE_MANAGER
   IF  (Touch(y) > 200 And Touch(y) < 240) THEN
     IF (TOUCH(x)>55 AND TOUCH(x)<155) THEN  ''OK
       RBox 55, 200, 100, 34, , BUTTON_COL, BUTTON_COL
       Text 105, 217, "OK", CM, 1, 2, 0, BUTTON_COL
       Do While Touch(x) <> -1 : SAMPLE_MANAGER : Loop
       OK_CANCEL_BUTTONS=1
       EXIT FUNCTION
     ENDIF
     IF (TOUCH(x)>165 AND TOUCH(x)<265) THEN  ''CANCEL
       RBox 165, 200, 100, 34, , BUTTON_COL, BUTTON_COL
       Text 215, 217, "CANCEL", CM, 1, 2, 0, BUTTON_COL
       Do While Touch(x) <> -1 : SAMPLE_MANAGER : Loop
       OK_CANCEL_BUTTONS=0
       EXIT FUNCTION
     ENDIF
   ENDIF
 LOOP
END FUNCTION
 
SUB DRAW_NUMPAD(BUTTON_START AS INTEGER,TITLE AS STRING)
 CLS(BG_COL)
 DrawButton BUTTON_START,0, 66, 195, 60, 40, BUTTON_COL, "0"
 DrawButton BUTTON_START+1,0, 130, 195, 60, 40, BUTTON_COL, "1"
 DrawButton BUTTON_START+2,0, 194, 195, 60, 40, BUTTON_COL, "2"
 DrawButton BUTTON_START+3,0, 258, 195, 60, 40, BUTTON_COL, "3"
 DrawButton BUTTON_START+4,0, 130, 150, 60, 40, BUTTON_COL, "4"
 DrawButton BUTTON_START+5,0, 194, 150, 60, 40, BUTTON_COL, "5"
 DrawButton BUTTON_START+6,0, 258, 150, 60, 40, BUTTON_COL, "6"
 DrawButton BUTTON_START+7,0, 130, 105, 60, 40, BUTTON_COL, "7"
 DrawButton BUTTON_START+8,0, 194, 105, 60, 40, BUTTON_COL, "8"
 DrawButton BUTTON_START+9,0, 258, 105, 60, 40, BUTTON_COL, "9"
 DrawButton BUTTON_START+10,0, 66, 150, 60, 40, BUTTON_COL, "."
 
 DrawButton BUTTON_START+11,0, 2, 195, 60, 40, BUTTON_COL, "<-"
 DrawButton BUTTON_START+12,0, 2, 150, 60, 40, BUTTON_COL, "OK"
 DrawButton BUTTON_START+13,0, 2, 105, 60, 40, BUTTON_COL, "Cancel"
 TEXT 160,10,TITLE,CT,8,1,FG_COL,BG_COL
 
END SUB
 
 
FUNCTION NUMBERPAD(TITLE AS STRING) AS FLOAT
 NUMBERPAD=-1'invalid
 LOCAL STRING ENTRY
 ENTRY=""
 ''use high numbered buttons to not interfere with other screens
 LOCAL INTEGER BUTTON_START
 BUTTON_START=BUTTON_COUNT-15
 DRAW_NUMPAD(BUTTON_START,TITLE)
 DO
   SAMPLE_MANAGER
   BUTTON_PRESS=CheckButtonPress(BUTTON_START, BUTTON_START+13)
   IF BUTTON_PRESS >= 0 THEN
     CheckButtonRelease(BUTTON_PRESS)
     if BUTTON_PRESS = BUTTON_START THEN ENTRY=ENTRY+"0"
     if BUTTON_PRESS = BUTTON_START+1 THEN ENTRY=ENTRY+"1"
     if BUTTON_PRESS = BUTTON_START+2 THEN ENTRY=ENTRY+"2"
     if BUTTON_PRESS = BUTTON_START+3 THEN ENTRY=ENTRY+"3"
     if BUTTON_PRESS = BUTTON_START+4 THEN ENTRY=ENTRY+"4"
     if BUTTON_PRESS = BUTTON_START+5 THEN ENTRY=ENTRY+"5"
     if BUTTON_PRESS = BUTTON_START+6 THEN ENTRY=ENTRY+"6"
     if BUTTON_PRESS = BUTTON_START+7 THEN ENTRY=ENTRY+"7"
     if BUTTON_PRESS = BUTTON_START+8 THEN ENTRY=ENTRY+"8"
     if BUTTON_PRESS = BUTTON_START+9 THEN ENTRY=ENTRY+"9"
     if (BUTTON_PRESS = BUTTON_START+10) AND (INSTR(ENTRY,".") = 0)THEN ENTRY=ENTRY+"."  'only add one .
     if (BUTTON_PRESS = BUTTON_START+11) AND (LEN(ENTRY)>0) THEN ENTRY=left$(ENTRY,LEN(ENTRY)-1)
     IF ENTRY="." THEN ENTRY="0."
     IF LEN(ENTRY)>9 THEN ENTRY=LEFT$(ENTRY,9)     'truncate
     if BUTTON_PRESS = BUTTON_START+13 THEN NUMBERPAD=-1: EXIT FUNCTION
     if BUTTON_PRESS = BUTTON_START+12 THEN
       NUMBERPAD=VAL(ENTRY)
       IF OK_CANCEL_BOX("ACCEPT?",STR$(NUMBERPAD))>0 THEN EXIT FUNCTION  ''OK
       ''OTHERWISE CANCEL
       NUMBERPAD=-1
       DRAW_NUMPAD(BUTTON_START,TITLE)
     ENDIF
   ENDIF
   TEXT 0,45,RIGHT$("               "+ENTRY+"_",10),LT,8,2,FG_COL,HL_COLOUR
 LOOP
 
END FUNCTION
 
phil99

Guru

Joined: 11/02/2018
Location: Australia
Posts: 2135
Posted: 05:39am 20 Mar 2024
Copy link to clipboard 
Print this post

Welcome to the forum.
A possible work-around, until someone with more experience with GUI Controls chimes in may be to adjust the initial calibration values via the Editor, rather than the screen.
'' analog read on pin 4 for buck reg current shunt
CONST SHUNT_ADC=4
SETPIN SHUNT_ADC,AIN
DIM FLOAT I_CAL(5)
I_CAL(1) = -1/(100*0.1) '0.1# shunt with x100 gain same as 10# resistor. 1V => 100mA, result in A
I_CAL(2) = -1/0.015  'nominal 15m# shunt
I_CAL(3) = -1/0.015  'nominal 15m# shunt
I_CAL(4) = -1/0.015  'nominal 15m# shunt
In the 'comments for shunt resistances # = Ω

Skip the on-screen calibration and connect a multimeter in series with the output.
Record the multimeter current reading and the displayed current then exit the program and open the Editor (type edit or press F4)
Adjust the I_CAL(x) values by the ratio of the previous pairs of current readings.
Restart the program (type run or press F2) and check the values again. Repeat if necessary.

eg
I_CAL(1) = -1/(100*0.1) * multimeter_I / display_I 'or the other way around if that way makes it worse
.
Edited 2024-03-20 15:44 by phil99
 
jockmack
Newbie

Joined: 17/03/2024
Location: Australia
Posts: 5
Posted: 09:13am 21 Mar 2024
Copy link to clipboard 
Print this post

Thanks for the reply and info Phil.

I have done some more investigation & it seems I am getting an out of bounds error.

This came up on Tera Term as I entered calibration data.

[675] I_CAL(I_NUM)=-NEW_CAL
Error: Index out of bounds

I will post more info if I find out why this is occurring.

Cheers
 
phil99

Guru

Joined: 11/02/2018
Location: Australia
Posts: 2135
Posted: 12:05pm 21 Mar 2024
Copy link to clipboard 
Print this post

The error at line [675] I_CAL(I_NUM)=-NEW_CAL (which is in SUB DO_I_CAL(I_NUM AS INTEGER) about half way down) is probably due to the value of I_NUM exceeding the value specified in DIM FLOAT I_CAL(5) below. Try changing it to DIM FLOAT I_CAL(6)
'' analog read on pin 4 for buck reg current shunt
CONST SHUNT_ADC=4
SETPIN SHUNT_ADC,AIN
DIM FLOAT I_CAL(5)
I_CAL(1) = -1/(100*0.1) '0.1# shunt with x100 gain same as 10# resistor. 1V => 100mA, result in A
I_CAL(2) = -1/0.015  'nominal 15m# shunt
I_CAL(3) = -1/0.015  'nominal 15m# shunt
I_CAL(4) = -1/0.015  'nominal 15m# shunt

If that doesn't work the problem must be BUTTON_PRESS which sets the value that gets passed to I_NUM.
However this Sub limits the value of BUTTON_PRESS to the range 1 to 4 so perhaps you will need to ask SiliconChip.
SUB CALIBRATE_MENU
DRAW_CALIBRATE
DO
  SAMPLE_MANAGER
  BUTTON_PRESS=CheckButtonPress(0, 5)
  IF BUTTON_PRESS >= 0 THEN
    CheckButtonRelease(BUTTON_PRESS)
    IF BUTTON_PRESS = 0 THEN  'volts
      IF OK_CANCEL_BOX("ENSURE NO LOAD","ON TERMINALS")>0 THEN DO_V_CAL
      DRAW_CALIBRATE       'return
    ENDIF
    IF (BUTTON_PRESS >=1) AND (BUTTON_PRESS <=4) THEN  'I1
      DO_I_CAL(BUTTON_PRESS)
      DRAW_CALIBRATE       'return
    ENDIF
    IF BUTTON_PRESS = 5 THEN EXIT DO
  ENDIF
LOOP
DRAW_MAIN
END SUB
 
jockmack
Newbie

Joined: 17/03/2024
Location: Australia
Posts: 5
Posted: 02:24am 22 Mar 2024
Copy link to clipboard 
Print this post

Thanks again Phil for your feedback.

I have contacted SC and they said they will have a look, they did suggest reloading the firmware.

I did get time last night to do this and actually erased the PIC in the process but the same problem exists.

I don't know where that Integer (27) is coming from in the (I_NUM)value.

[675] I_CAL(I_NUM)=-NEW_CAL
Error: Index out of bounds


As you pointed out that Integer should only be in the range of 1 to 4.

I will give your change a try & see what happens.

I will see if SC comes up with anything and post it here

Cheers
 
phil99

Guru

Joined: 11/02/2018
Location: Australia
Posts: 2135
Posted: 03:35am 22 Mar 2024
Copy link to clipboard 
Print this post

If 27 is being inserted you would need to make the array CAL() bigger than that to avoid the same error.
Doing that would probably just create other problems.
 
erbp
Senior Member

Joined: 03/05/2016
Location: Australia
Posts: 192
Posted: 03:40am 22 Mar 2024
Copy link to clipboard 
Print this post

Interestingly in the last 2 images in the original post, the heading at the top of the screen says: "New I(27) Factor:". It seems it has already got 27 as the index into I at the time this screen is displayed!
 
jockmack
Newbie

Joined: 17/03/2024
Location: Australia
Posts: 5
Posted: 06:38am 22 Mar 2024
Copy link to clipboard 
Print this post

Thanks erbp

I have done some further testing and I(27) value appears to come from a button press on OK after selection of I value.

The value of I_CAL(I_NUM)=-1 right up until the OK is pressed.

Possibly from something in OK_Cancel_Buttons function?

I don't know how it's connected but that spurious "27" value is injected at that point.
 
JohnS
Guru

Joined: 18/11/2011
Location: United Kingdom
Posts: 3801
Posted: 11:01am 22 Mar 2024
Copy link to clipboard 
Print this post

In case it's relevant, 27 is the code for the (ASCII) Escape character.

Could it be from Cancel?

John
 
jockmack
Newbie

Joined: 17/03/2024
Location: Australia
Posts: 5
Posted: 09:25pm 22 Mar 2024
Copy link to clipboard 
Print this post

Thanks for the input JohnS, you're suggestion of ASCII escape character is probably correct.

In running the program on the backpack and stopping and interrogating the value of

Button_Press is equal to -1 for each of the 1 through 4 current buttons.

It changes to 27 after input of any value except zero and selecting OK but before OK

at the Select window.

This part of the code starting at Line 321 should input value of 1 through 4 but

doesn't appear to be so. It does work for volts though with a 0 value returned.

SUB CALIBRATE_MENU
 DRAW_CALIBRATE
 DO
   SAMPLE_MANAGER
   BUTTON_PRESS=CheckButtonPress(0, 5)
   IF BUTTON_PRESS >= 0 THEN
     CheckButtonRelease(BUTTON_PRESS)
     IF BUTTON_PRESS = 0 THEN  'volts
       IF OK_CANCEL_BOX("ENSURE NO LOAD","ON TERMINALS")>0 THEN DO_V_CAL
       DRAW_CALIBRATE       'return
     ENDIF
     IF (BUTTON_PRESS >=1) AND (BUTTON_PRESS <=4) THEN  'I1
       DO_I_CAL(BUTTON_PRESS)
       DRAW_CALIBRATE       'return
     ENDIF
     IF BUTTON_PRESS = 5 THEN EXIT DO
   ENDIF
 LOOP
 DRAW_MAIN
END SUB


Footnote added 2024-03-23 07:33 by jockmack
I think its a fault in the touch circuit sending faulty values back through SPI.
 
Print this page


To reply to this topic, you need to log in.

© JAQ Software 2024