Indevin https://rodyne.com Mon, 23 Jun 2025 02:41:10 +0000 en-NZ hourly 1 https://wordpress.org/?v=7.0.7 https://rodyne.com/wp-content/uploads/2015/11/web-header2.png Indevin https://rodyne.com 32 32 Continuous Floatation in Wineries https://rodyne.com/?p=2209 Thu, 10 Apr 2025 04:17:25 +0000 https://rodyne.com/?p=2209 Continue reading →]]> There are two common methods to clarify juice before it goes into fermentation. Firstly there is cold-settling, where you just leave the juice alone for several days and wait for the clear juice and lees (the juice particulates) to separate. Then there is floatation, where you add a processing agent (usually bentonite) and bubble a gas (nitrogen) through the juice and allow the particulates to rise to the top and the clear juice at the bottom. For big wineries, floatation is preferred as it reduces the long settling times (where juice may even begin to ferment before it is ready) and it produces a clearer juice faster.

Me, Allison (Winemaker) and Fran (transformation project manager) overlooking the Indevin Winery floatation system and three of its four float pools

This post will concentrate on this floatation process, or more specifically the continuous floatation process, and how your wine-making system should keep track of the composition. A very simplified diagram of this process is below. Where multiple Crush tanks with compatible juice are successively fed into the pool until the clear tank is full.

Note in the text below we might see reference to a propagation or “Prop” tank, this is just another clear tank with a different name and a different status and the procedure to get its composition is identical.

When processing just a single crush tank, we fill the float pool from the crush tank (yellow in picture), and once the pool is full we begin bubbling the gas through, collect the clarified juice from the bottom (blue in picture) into the clear tank, and the rest (the grey in the picture) is considered lees and goes to the lees tank where it is either further clarified to get a bit more juice or disposed of. Once the crush tank and float pool are empty we start again.

The problem with this (single) float is we have to stop the process to empty the pool, which can add some hours to the procedure. In a continuous floatation process when the first crush tank is emptied, we record the tank dips, and flowmeter readings, while the float pool is still full and change to a new crush tank and continue the process with minimal interruption, eventually after processing several similar crush tanks the last job empties the pool and the sequence restarts with the next batch.

Obviously with this continuous system it is still vital to keep track of the composition of the wine for food safety and regulatory requirements. Because the float pool is much smaller than the size of the crush tank we can make a couple of important assumptions verified by measurements:

  1. The juice volume left in the float pool from the last crush tank will be completely replaced by the juice from the next crush tank.
  2. The volume that goes into lees will be the crush volume minus the clear volume.

To work this into a system we need to be able to capture the volumes in and out of the pool, which we do using a flowmeter (FM1) on the inlet of the pool from the crush tank and another (FM2) on the clears line. There is no point in putting a flowmeter on the lees line, as the volume is mainly particulate and foam and confuses normal flowmeters which require liquids. Generally lees volume is 6% of the crush total, but this is an average based on an average juice, and there is no such thing as one vintage may produce differing results. So at the end of each crush tank we have

FM1 = Crush Volume 
FM2 = Clear Volume 

Lees Volume = FM1 - FM2

The float pools also have a nominal (full) volume we work to and in practice the inflow and outflow is controlled to keep it at this volume throughout the process.

For the system we designed we split the continuous floatation into jobs based on the emptying of a crush tank. At the start of each job we will have the known start volumes of the crush and clear tank (via DIPs or otherwise) and our first action is to ensure both the flowmeters are reset so we can do the calculations. This I do quite easily as I already have a bi-directional service connecting the SCADA systems to MySQL (the Bulls-i RDBS) plus I keep my own counters for both flowmeter values independent of the values held in SCADA as these can be reset manually and I have been burnt many times by using other peoples data.

We have three possible types of continuous floatation jobs, the first where we fill the pool, then one or more middle jobs, and finally an end job where we flush the pool and this can be seen in a simple series of jobs below.

In the first job in the series where we fill that tank we only have one source “SRC #1” and all the juice volume will end up in either

  1. The Float Pool (at the Float pool fill volume)
  2. The Clear/Prop tanks (Volume Determined by FM2 and the Initial and Final Dips of the tanks)
  3. The Lees Tank (the remainder with the “dirty” juice)

This is a normal job, one-to-many and fits in nicely with the current winemaking job system.

For the middle job(s) in the series however it becomes a bit more complicated as we have two source vessels “SRC #2” and “Float Pool #1” and two or more destination vessels as per the previous case. This situation cannot be processed as a normal job which must be one-to-many, or many-to-one to create a composition. to fix this at the end of the job we need to create two jobs.

The first job is a prequel job to transfer (flush) the volume of “Float Pool #1” to the clear and the lees, this will also alter the volumes of the two tanks which we need to ensure are the start volumes of the second job. The second job is the same as the first job in the series where we again fill the pool from the source and have balances go to clear and lees.

The final job in the series where we empty the float pool again has two source tanks, but the only difference with this one is that at the end of the job, all the volume in the float pool is flushed out to the clear and lees vessels so ending the sequence.

In the Bulls-i system “Float Pool #1” is an asset, but it is NOT a tank. However as a programmer, I can bypass this restriction and so treat it as a virtual tank, giving it volume, batch-codes and a composition exactly like a normal vessel and knowing that at the end of the series it will be empty.

So not a difficult process when you break it down into steps.

There are two users of the system, the winemaker who creates the floatation jobs and decides on which tanks and float pools will be allocated, where they go in the sequence, and what additives if any need to be made. The other is the cellar-hand who operates the system and does the work on the winery floor, and who needs to see the current status of the job clearly and be able to enter information on a system to say which tanks are connected and their opening and closing volumes.

The winemaker part of this is shown above (for “Pool B”) Only one job can be active in a line, and we must have at least a FIRST (to fill the pool) and a LAST (to empty the pool) along with 1 or more MIDDLE jobs. The Winemaker job screen looks like below

This job translates onto the Cellar-hand operators touchscreen as below where we have a very limited number of options to start the job, make product additions to the tank/pool and start and end each tank in series. The operator screen will also require RFID confirmations from operators who have the qualification to do this. In this way the winemaker fills the operators work buffer and when the operator has finished and signed off the job it is returned to their office screen to complete.

All the flowmeter and SCADA volumes are captured and displayed real-time on the appropriate graphic on the screen to aid both the winemaker (sitting remotely in an office) and the operator doing the job. There are also a few additional toys we have added to this system to reproduce the four (RED/GREEN/YELLOW/FALSHING) LEDs from the program to real lights on the cellar floor.

Hopefully this brief introduction to automating a slightly more complicated production process will show what is possible.

]]>
Flip, Flap, Flop! – Solari Fix. https://rodyne.com/?p=2169 Tue, 01 Apr 2025 01:28:15 +0000 https://rodyne.com/?p=2169 Continue reading →]]>

About 13 years ago, before this web site, and back when I used to be called boznz.com, I put out a simple project to reverse engineer some soft-flap display units (brand-name Solari). My clients CEO had bought 20 of them at a charity auction when they were upgrading the flight information boards at Auckland Airport (18 worked!).

I did a rough write up at the time that got featured on hack-a-day and I preserved the basics of the original web page here.

A Sad, Broken, Display 🙁

It was happily sitting pride of place in the cafeteria of my clients 15-Valley winery for those 13 years providing real-time updates to the grape tonnage received, and the occasional funny message, in its own retro-spectacular way. This was until a few weeks ago, when it failed to wake up at the start of vintage.

Actually I was quite surprised it lasted that long given how old and fragile the individual units are, how quick and dirty the whole project was engineered, and how bad an environment it was in. So, as support for vintage was ticking over nicely, and ignoring my usual warranty rules, I got the guys to uninstall it and throw it in the back of my dodgy electric pickup for the bumpy 20KM journey back to my workshop.

After an age with an air compressor and vacuum cleaner, and with the unit fully dismantled in front on me on the bench now, the memories flooded back. I had just returned to electronic engineering in from a 15 year hiatus in IT and network management and my electronics lab at that point consisted of an old radio shack soldering iron, a fluke multimeter, a 5Mhz analog scope, a newly acquired EasyPIC 4 development kit from Mikroelectronika with MikroPascal for PIC and various out of date through-hole components and breadboards, that managed to survive shipping from the UK. All of course in my spare bedroom, so when they guys at Indevin dropped the box of displays on my desk and asked me to “make it work” I was all fired up to give it all a try. I managed to get something knocked-up and working, good enough, at least that a carpenter could mount it all in a neat cabinet and hide the warts.

Fast forward 13 years..

With the unit in pieces I immediately saw the problem, the control board, and four of the units had pretty severe water damage. I say water, but it was more likely beer, wine or a soft drink as it had eaten away at some of the PCB’s and mechanics. But 14 of the display units looked good. The problem was the control board needed replacing and as this was the only time I ever used the MikroPascal compiler for PIC in anger, so it was going to be a new design.

I remember at the time I thought MikroPascal was quite cool, and it was dead simple, solving me the problem of re-learning C. Alas it was a short love affair as my next client insisted I use C and from that point on, so well I just went with the money!

Anyway, armed with the original pascal source code I quickly knocked out a replacement control board and 140 line of (XC8) C which would replicate the functionality. While in the re-design phase, changes were made to convert the input from ethernet to RS485, so the weighs could be directly updated from their SCADA Interface. I reused the original transformer, cables and 14 of the remaining displays and got it all working again, but while it was in pieces I thought I would capture some nicer pictures.

Some Technical Info for the future.

The Control board mainly consists of power-supplies. The two raw 18VAC supplies combining and going directly out to the slave devices for 36VAC total. And two DC supplies, a 12VDC 3A which supplies the Solari slave devices, and a 5VDC 3A which supplies the control logic and the slave daughter boards. The 3A for the 5V is totally overkill, 20 slaves plus the control board draw a tenth of that, but it always good if you can re-use the same components to reduce the pick-n-place component count. The only other real change, apart from the obvious fact we are now using a serial RS485 data input instead of Ethernet, is that the front panel indicator/reset switch actually powers off the 12VDC and 5VDC supplies giving a harder reset. I also re-used another PIC16F1782 from my pile, which now wont go into landfill when I die 🙂

The video shows everything back in place but with a few calibration issues still to fix after dismantling and re-assembling the units (Yes I got the damaged ones working!) There must have been dozens of “Solari Technicians” back in the old days whose only job was to fix and re-calibrate these.. arghhhh!

The one thing of note in the code below is that I have extended the delay between sending packets out to the individual devices; This is to reduce the demand on the 12VDC and 18VAC supplies as the slaves only draw significant power when enabled and ‘flapping’. Moving the delay from 100mS to 600mS will space the demand out, and keep everything less stressed but will take 8 seconds to send out the message and so about 12 seconds to display all 18 digits.

I did not make any changes to the daughter board or its original pascal code. I saw a few minor improvements I could have made but did not see anything that really justified messing with them.

// SOLARI CONTROLLER Using PIC16F1782 https://rodyne.com/?p=1781
#include <xc.h>

#pragma config FOSC     = INTOSC    // Use internal 1% Oscillator should be good enough
#pragma config CLKOUTEN = OFF      
#pragma config FCMEN    = ON       	// osc fail monitor on
#pragma config MCLRE    = ON        // Used as reset button not IO
#pragma config WDTE     = ON     	// Watchdog timer ON
#pragma config STVREN   = ON        // reset on stack overflow ON
#pragma config BOREN    = ON        // reset on stack low voltage

#define BUS_DAT(state)  LATAbits.LATA0 = state // 1 = ON, 0 = OFF
#define BUS_CLK(state)  LATAbits.LATA2 = state // 1 = ON, 0 = OFF
#define ACT_LED(state)  LATBbits.LATB3 = state // 1 = ON, 0 = OFF (Activity LED)

#define _XTAL_FREQ  16000000 // req for the __delay_ms() function to work accurately
#define MAX_DEVICE  18

// The solari devices we use have 40 possible flap positions and there are several 
// Solari models with different "Character sets" so be aware if you need to use another type.

const char posn[40] = " _0987654321-/ZYXVUTSRQPONMLKJIHGFEDCBA"; // not used in code but just for info

const char ascii[128] = // map ascii char to its Solari position to simplify code (unsupported chars map to space)
{
    0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,
    12, /* - (dash) */
    0,
    13, /* / (forward-slash) */
    2,11,10,9,8,7,6,5,4,3, /* 0..9 */
    0,0,0,0,0,0,0,
    39,38,37,36,35,34,33,32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14, /* A..Z */
    0,0,0,0,
    1, /* _ (underscore) */
    0,
    39,38,37,36,35,34,33,32,31,30,29,28,27,26,25,24,23,22,21,20,19,18,17,16,15,14, /* a..z */
    0,0,0,0,0
};

// calibration allows for bad alignment on a unit and allows us to bump the char back
// or fwd. Solaris count backwards so A comes after B just to make it more confusing
const int8_t DefCal[4][MAX_DEVICE] =
{
    { 0,0,-1,0,0,0,0,0,0, 0,0,0,-1,0,0,0,0,0 }, // Offset for CharNo 0..10
    { 0,0,-1,0,0,0,0,0,0, 0,0,0,-1,0,0,0,0,0 }, // Offset for CharNo 11..20
    { 0,0,-1,0,0,0,0,0,0, 0,0,0,-1,0,0,0,0,0 }, // Offset for CharNo 21..30
    { 0,0,-1,0,0,0,0,0,0, 0,0,0,-1,0,0,0,0,0 }  // Offset for CharNo 31+
};

volatile int8_t   calibration[4][MAX_DEVICE];  // calibration offsets in case solari is out, 0, -1, or +1 allowed

volatile uint8_t  SerBuf[MAX_DEVICE+1]; // allow for trailing CR on string
volatile uint8_t  RxBufPtr=0, RxTimeout=0, LastChar=0, tick=0;

void __interrupt() ProcessINT() // UART RXD interrupt (ON RC7) and TIMER
{
    if(RCIF) // when serial char received
    {   
        LastChar = RCREG;
        if(RxBufPtr<MAX_DEVICE) SerBuf[RxBufPtr++] = LastChar;
        RxTimeout=250; // start data timeout 250mS
        RCIF=0; // flag RX interrupt processed
    }
    if(TMR0IF) // approx every 1mS
    {
        if(RxTimeout>1) RxTimeout--;  // decrement to 1
        TMR0IF=0; // flag interrupt processed        
    }        
}

void delay_us(uint8_t us) // hard coded for 16Mhz (approx is OK)
{
    while(us--)
    {
        asm("NOP"); asm("NOP"); asm("NOP"); asm("NOP"); 
        asm("NOP"); asm("NOP"); asm("NOP"); asm("NOP");
    }    
}

void main()
{
    int8_t    DevNo, i, SolariCharNo;
    uint16_t  mask, data;

    OSCCON = 0b01111010; // 16Mhz Internal OSC
    WDTCON = 0b00010101; // WDT ON timeout 4 seconds inactivity
    
    // I/O Ports
    TRISA   = 0b00000000; // Port A Direction reg All outputs
    ANSELA  = 0b00000000; // Port A Analog config No analog
    LATA    = 0b00000000; // Port A Initial state All OFF
    TRISB   = 0b00000000; // Port B All output
    ANSELB  = 0b00000000; // Port B All digital
    LATB    = 0b00000000; // all off
    TRISC   = 0b10000000; // Port C All outputs except RC7 which is RXD
    LATC    = 0b00000000; //     

    ACT_LED(0); 
    BUS_CLK(0);
    BUS_DAT(0);                
	
    BRG16 = 0;  // Set up UART 9600/8/N/1 Async (CREN=1, SYNC=0, SPEN=1)
    SPBRG = 25;        
    SPEN  = 1;  // enable serial
    RCIE  = 1;  // enable UART1 interrupt
    PEIE  = 1;  // Enable Peripheral interrupt    
    
    OPTION_REG = 0b10000011; // Set up TIMER0 for 1mS interrupt 16M/4/16/256 = 976Hz
    TMR0IE     = 1;          // enable timer 0 interrupt

    for(DevNo=0;DevNo<4; DevNo++)
        for(i=0;i<MAX_DEVICE; i++)
        {    
            calibration[DevNo][i]=DefCal[DevNo][i]; // copy default calibration for each slave
        }      
    
    while(1)
    {        
        ACT_LED(1); // Default is on, off while processing
        CREN=0;     // clear any rec errors (if any)
        RxBufPtr=0; // clear receive buffer
        LastChar=0; // reset last char so we dont re-trigger
        RxTimeout=0;// reset the timeout
        CREN=1;     // re-enable receive cct (with any errors cleared)
        GIE=1;      // turn interrupts back ON
        
        while(RxTimeout!=1 && LastChar!=0x13) // wait for serial data event
        {    
            CLRWDT();            
        }   
        
        ACT_LED(0); // change to show activity        
        GIE=0;      // turn off ALL interrupts while we process the message buffer
                
        if(LastChar==0x13) // Got VALID DATA + CR
        {            
            for(DevNo=0; DevNo<MAX_DEVICE; DevNo++) 
            {
                // converting ASCII char received in buffer to a solari position to send to device
                SolariCharNo = (int8_t) ascii[SerBuf[DevNo]]; // look up the character position
                i=0; // default calibration set to use for chars 0..10
                if(SolariCharNo>10) i=1; // calibration set to use for chars 11..20
                if(SolariCharNo>20) i=2; // calibration set to use for chars 21..30
                if(SolariCharNo>30) i=3; // calibration set to use for chars 31..40
                SolariCharNo = SolariCharNo + calibration[i][DevNo]; // adjust back or forward if required to fix misaligned chars
                if(SolariCharNo>40) SolariCharNo=0; // cannot have SolariNo more that 40 so wrap around to 0
                if(SolariCharNo<0) SolariCharNo=40; // cannot have SolariNo less than 0 so wrap around to 40
                data = (uint16_t) ((SolariCharNo*256) + (DevNo+1)); // slave written in pascal so device numbering starts from 1 not 0
                
                if(DevNo%2) ACT_LED(0); else ACT_LED(1); // flash LED during activity
                
                mask = 0x0001;
                for(i=0; i<16; i++) // clock out the string to the device chain
                {
                    CLRWDT();
                    if(data & mask) BUS_DAT(1); else BUS_DAT(0);
                    delay_us(10);
                    BUS_CLK(1);
                    mask = mask << 1;
                    delay_us(10);                    
                    BUS_CLK(0);
                }    
                __delay_ms(600); // bigger delay between chars so they don't all flap at the same time (too many amps!)
            }            
        }

    }
}

A better view of the PCB is here. The only error I had was I added a pull-up resistor R13 in the original version 1 board on the TPS5430 ENA to Vin when it needs to float to enable and grounded to sleep, the design I have now removed this and works fine.

]]>
Removing Some Very Un-Corporate Baggage https://rodyne.com/?p=1883 Tue, 11 Mar 2025 03:35:06 +0000 https://rodyne.com/?p=1883 Continue reading →]]>

In preparation for the shut-down and hand-over of the outgoing Bulls-i Winemaking system at Indevin I thought I would go through and maybe remove some of the more inappropriate comments and messages so as not to offend the next guy who has to learn from this. Many of the messages are aimed at me to get the appropriate attention from the user so they will actually email me the error so I could check what happened. Snippy here was a good example, that used to pop up after particularly nasty transaction failures.

A quick search for the word “fuck” in the source code brings up 57 hits, most of these are comments on how stupid I was at the time, but several translated into actual error messages or screens below. Many more error messages are so deeply embedded in the system they are actually impossible to trigger manually with out a specific set of weird circumstances (Which I guess was why I put them there) like the ones below which were probably only ever seen by one or two people, if any!

raise exception.Create('Having problems determining crush job to use. 
                        May be the result of a manual float job or multiple jobs filling 
                        the same crush tank, cosmic rays, aliens, who fu**ing 
                        knows!'+cr+cr+
                       'To-Do Boz to un-couple float jobs from crush pool and never-ever 
                        try to make sensible assumptions where winemaking is 
                        concerned.'
                       );
raise exception.create('Could not locate i-Ticket '+cfg.bookingid+cr+cr+
                       'Possibly hiding with Elvis or the Easter Bunny!'); // WTF
messageDLG('Job is no longer valid!'+cr+cr+
           'Yes this is totally weird and not normal, the job may have been deleted 
            by another user, network issues or Space Aliens (hell who knows!) either 
            way this session is fucked..'+cr+cr+
           'Bulls-i will now give up on life and die a gruesome death!'+cr+cr+
           'Definitely A Good time to restart your computer and take a coffee break!',
           mterror,[mbok],0);
raise exception.Create('Crush Tank No Longer Exists. Elvis has left the Building'+cr+cr+
                       'Bulls-i is now going to give up and move to a monastery in
                        Tibet somewhere, where it will hopefully never see a                                         
                        wine-maker again!'); 

A few others were more easy to trigger.

The Classic one I suppose is the infamous “Fuck-up Screen” which I used to show to the intake co-ordinators when they ignored the three steps before booking a truck onto a bin and subsequently had to back the load out. It had to post a message to a SCADA service and wait for a response, hence the little progress bar. I even kept a leaderboard of who made the most fuck-ups in a vintage. Luckily the guys doing the intake coordination had broad shoulders!

With the system being transactional in nature, there were quite a few No-No’s that I really should not have had buttons for. These I would generally lock down to me only. One such example to the right, was was trying to re-run a composition on a closed Job.

Bulls-i did not use unit tests, and with 140K lines of code I was not going to retrofit them. However it did have a very complicated set of initialisation code. The failure message (which thankfully not many clients ever saw) was this.

And of course which Indevin winemaker could ever forget the Winemaker “Job Issue” warning screen. The current iteration of this screen has the very tame Dr Evil picture as below:

But before Dr Evil, we had many others, here are a few of the more memorable ones:

The Donald Trump “You’re Fired!” meme was actually back from 2014, before he was the president of the USA, and when people still smiled at his television antics.

Kim Jong Un. this meme only lasted on the screen for about a week until somebody pointed out he actually looked a little bit like our laboratory manager Owen (He doesn’t!). Luckily Owen himself did not actually make the connection.

And who could forget Chuck Norris. As an aside, don’t ever Google Chuck Norris, (I’m sure there’s a Chuck-Norris’ism there about breaking the Internet, but I cant think of it) anyway, you will go down a rabbit hole for hours as you re-live the old memes and some new ones. I just had to include the meme collage below, even though they are not really related to this post..

And finally which IT Person can forget the Test Print Labels that used to spit out of various printers, usually at the most inappropriate times. This was also the winemaker warning meme for all of about a day, before the HR police saw it. Luckily the HR Department never tested printers!

I really could go on and on about this, but my daughter is a DEI coordinator for a international university and I really wouldn’t want her to get too annoyed or embarrassed with me (sorry Alex!) and although the system never-ever actually crossed any lines it probably stepped on it several times. The one I remember well, as I had to purge about a 150 dodgy records, was because at one point we had the ability for the users of the system to put quotes of the day into it without any forms of checks, these often never came up for weeks, and then came up at the most inappropriate time. Obviously I learnt that lesson the hard way.

I know my system is not unique in having these, and most, if not all the thousands of users of the system found these totally fine. Many people still ask where the funny messages and menu items have now gone, so I guess this post is for them, and as a legacy of a pre-corporate environment, one that is unlikely to ever return.

]]>
Yet another boring small project (with an interesting back-story) https://rodyne.com/?p=1781 Wed, 05 Mar 2025 07:13:08 +0000 https://rodyne.com/?p=1781 Continue reading →]]> As I sit at my desk in the clients winery, “working” vintage support, (a job where I mostly feel like Dumbo’s feather, ie a prop that gives the company confidence everything will be great, where as in reality I do very little. The client knows my feelings on this but are quite happy to pay for those just in case moments that might prove costly). Anyway, I thought, I need something big, which these guys can remember me by when I’m gone, and looking around the winery building I wondered what I could possibly contribute that would be both useful and memorable, and I thought a clock!

So I thought I would design and build a clock, one which will reliably keep the time and be pretty well maintenance free for its hopefully long and boring life. But why would anybody need to design a clock today, surely every variation has been tried? Well, for one thing, this clock is going to be pretty big. Although it is only a seven-segment display, with two digits for hours and two for minutes, each segment is going to be a wine bottle. So 7 bottles are required per digit, in total 28 bottles.

Inside each bottle will be an LED strip or similar so we can light the segment, therefore we need a circuit board that can drive 28 LED strips, also, to make the flashing colons that separate the hours and minutes, the ends of two bottles will be used, and so another output is required to drive the LED’s in those. That’s 29 drivers, 29 bottles, 2M x 1M in size minimum, and as a total package with some form of mounting, quite heavy I guess!

The boring electronics prototype

The electronics for this were always going to be easy. Again it’s a project I would normally slap in an old PIC microcontroller and an RTC and have it designed in a few hours. But that was the old 1990’s Boz, the new 2025 Boz is on a mission to try out some of the new technologies out there, and the modern micro-controller for today will be (rolls-the-dice..) the ESP32-C3. Specifically the ESP32-C3-WROOM-02 a RISC-V 32-bit processor with Wi-Fi, Bluetooth, 400KB of RAM and 2MB of Flash all for $3.30. (A bit over-kill for a clock, but welcome to 2025!)

So a brand-new processor I have never used before, or even seen before a few days ago should be exciting, or at least a bit risky? Maybe, maybe-not. Lets go on a journey together. For this journey, the AI of choice will be Googles Gemini 2.0, a competent, if pretty boring and sensible AI, so while Grok 3 may be considered your rebellious genius teenager, Gemini 2.0 is probably your quiet old uncle, the retired school teacher who lives in his shed and doesn’t really talk to many people, but also tends to be sensible.

The Schematic.

Yes, pretty boring and certainly not rocket science. An RTC, a serial input for a GPS (Not yet sure if I can get a signal where it is going to be located, but a good time-source never-the-less), and 29 open-collector outputs that can drive a few hundred milliamps at up to 24V, total BOM cost around $8 from LCSC

Firstly, it is obvious that a lot of the big AI models have a good understanding of electronics, and simple questions, like how much power will this or that draw? or what are the maximum and minimum supply voltages for power and IO? are generally answered correctly. However, that accuracy is about 90% at best and 10% of answers will be confidently answered wrongly, with a quick and humble apology when the mistake is finally pointed out. Again the ability to feed a schematic into a AI would be fantastic, but we are not there yet. (or if we are I haven’t seen it!)

Asking questions like, what capacitance would I require on VCC1 to keep the RTC running for 8-hours in the configuration shown was answered quite well with suitable workings out shown and the real-life circuit performing slightly better than the calculation

There are a few grey areas out there which may lead to said AI’s not being too accurate, for example, if I decided to use the USB interface that the ESP32-C3 has for program/debug, would I still require the boot button to program it? The reference design does show almost every possible IO pin used for programming and makes you wonder if there would be any left afterwards. People building these boards on the internet tend to favour the serial port and use of the DTR and RTS of a serial to USB driver to program it, well I’m telling you now, two wires to a USB connector are all you need and you do not need to press the boot or reset switches ever. Also as I used my usual GPIO expansion technique for slowly changing output ports by using a series of shift registers so that was never an issue..

Programming.

This is where it got interesting. After bigging up the project to Gemini (it felt right to do so!), I told it I was using the Arduino IDE (as the VSCode extension for the ESP32 was, to say it politely, work in progress!), I got it all excited to help me. I then fed the following prompt into him/her/it..

The ESP32-C3 is on a ESP32-C3-WROOM-02 board

The connections from the ESP32-C3 to the DS1302 are:

I/O (Pin 6 of DS1302) to GPIO 0
CE (Pin 5 of DS1302)  to GPIO 1
SCLK (Pin 7 of DS1302) to GPIO 3

The clock will output its time in the format HH:MM to a seven-segment display. It does this by sending 32 bytes of (7 segment) data out to four 74HC595 shift registers in series, the first shift register is hours x 10, the second shift register is hours x 1, the third shift register is minutes x 10, the last shift register is minutes x 1, The connections from the ESP32-C3 to the shift registers are as follows:

74HC595 pin 14 (DS) of first shift register to GPIO 6
74HC595 pin 12 (STCP) of all shift registers to GPIO 7
74HC595 pin 11 (SHCP) of all shift registers to GPIO 5

The 7-segments are connected to each 74HC595 as follows

Q0 = segment a
Q1 = segment b
Q2 = segment c
Q4 = segment d
Q5 = segment e
Q6 = segment f
Q7 = segment g

I want to be able to connect to the ESP32 Wi-Fi and set the time on the DS1302 through a web page, then update the LED outputs of to the shift register every time it changes

I would like to understand the code I am writing by not using any libraries where possible e.g. the DS1302 communication should be easy without libraries, though the Wi-Fi stuff might be a bit hard for me to understand.

Is this enough to go on?

I literally downloaded the Arduino IDE, downloaded the ESP32 extension for it, copied the code it generated from the above prompt in and bugger me if it didn’t just work!

Well, almost! It did kind of work, the clock counted and showed the time, the web-page worked and set the time, but of course the devil is always in the detail, the web-page was bloated and overkill and pretty ugly, it also only set the time, not the day and date, which I would require latter for daylight-savings calculations. Also as I lay there thinking about it, I thought, why would I want to type in the time from a web-page when I had a phone that already had the correct date and time down to the microsecond? I quickly corrected my prompt, asked it to separate out the networking part, into another module, change the Wi-Fi client into a Wi-Fi access point, made the web page into a simple button and bit of java-script to send the time/date, and manually tidied up the landing page a bit (All using AI prompts of course as I am really totally shit at this web stuff!)

As a side rant, lets talk again about that bloat! 400KB Ram and 2MB ROM and 75% used. I wont even try to understand the reasoning behind the colossal use of resources, lets just say we are in 2025 and the future seems to be bloat! Seriously, nobody these days seems to give a rats-arse about throwing in libraries that go into thousands of lines of code they have no idea what any of it does. Sure it makes my life easier, but extrapolating the extra computation and resources required by computers these days the waste of power must be several power stations! Anyway, rant over, back to the project..

The Arduino IDE itself is pretty bullet-proof; as I remember it from about 10 years earlier simply selecting the ESP32C3 dev module as my board, and setting the “USB CDC on Boot” to get debug information was all that was required, all refreshingly easy.

OK, so by now you have noticed the lack of pictures of the actual epic bottle-hardware! well, this is only the first part of the post as I still have to mount those 28.5 wine bottles (the point five being the two bottle-ends for the colon) on a very large mount, shove the LED’s in them, and get a 24V power supply as everything so far has been with USB power and logic probes. A few friends have offered to help in this part as my artistic flair is about as shitty as my web skills!

The code to the working board so far is below. It may change but as it stands it works as a stand-alone clock (just add big LED’s!)..

To be continued..

IndevinClock.ino

// Colabaration between Boz and various AI's that provides the logic to 
// manage the DS1302 real-time clock and update the display through the
// shift register GPIO expander. And provide a web portal to set the time.
// Full Hardware design is/will be at rodyne.com
// All Designs relating to this project are Public Domain.

#include <Arduino.h>
#include "networking.h"

// Pin Definitions
#define DS1302_CE_PIN 1  // Chip Enable
#define DS1302_IO_PIN 0  // Data I/O
#define DS1302_SCLK_PIN 3 // Serial Clock
#define SHCP_PIN 5  // 74HC595 Shift Register Clock (SHCP)
#define DS_PIN 6    // 74HC595 Data Input (DS)
#define STCP_PIN 7  // 74HC595 Storage Register Clock (STCP)
#define COLON_PIN 10  // flashing colon output
#define DSTLED_PIN 4  // debug led

const char * DayOfWeek[] =  { "ERR!","Mon","Tue","Wed","Thu","Fri","Sat","Sun" };

const uint8_t Lookup7seg[] = { 0b00111111, 0b00000110, 0b01011011, 0b01001111, 0b01100110, 0b01101101, 0b01111101, 0b00000111, 0b01111111, 0b01101111 };

uint8_t RTCsec, RTCmin, RTChour, RTCday, RTCmonth, RTCyear, RTCdow, Colon; // global variables visible to networking module
uint8_t LastUpdate, LastMinVal; // local to this unit


// Function Prototypes
void ds1302_write(uint8_t address, uint8_t data);
uint8_t ds1302_read(uint8_t address);
void ds1302_setTime(uint8_t sec, uint8_t min, uint8_t hour, uint8_t day, uint8_t month, uint8_t year, uint8_t dow);
void ds1302_getTime();
uint8_t bcdToDec(uint8_t val);
uint8_t decToBcd(uint8_t val);
void displayTime(); 
bool isDaylightSavings();

bool isDaylightSavings() 
{
  // New Zealand Daylight Saving Time (DST) rules: Starts: 2:00 a.m. on the last Sunday in September. Ends: 3:00 a.m. on the first Sunday in April.

  if (RTCmonth < 4 || RTCmonth > 9) 
  {
    return (RTCmonth > 9); // DST is active from October to March
  } 
  else if (RTCmonth == 4) 
  {
    // Check if it's the first Sunday in April
    int firstSunday = 8 - RTCdow; // Calculate the date of the first Sunday
    if (RTCday < firstSunday) 
    {
      return true; // DST is still active before the first Sunday
    } 
    else if (RTCday == firstSunday) 
    {
      return (RTChour < 3); // DST ends at 3:00 a.m.
    } 
    else 
    {
      return false; // DST is not active after the first Sunday
    }
  } 
  else if (RTCmonth == 9) 
  {
    // Check if it's the last Sunday in September
    int lastSunday = 30 - ((RTCdow + (30 - RTCday)) % 7); // Calculate the date of the last Sunday
    if (RTCday > lastSunday) 
    {
      return true; // DST is active after the last Sunday
    } 
    else if (RTCday == lastSunday) 
    {
      return (RTChour >= 2); // DST starts at 2:00 a.m.
    } 
    else 
    {
      return false; // DST is not active before the last Sunday
    }
  }
  return false; // Should not reach here
}

void displayTime() 
{
  uint32_t displayData = 0;
  uint8_t  rawHour = RTChour;

  if(isDaylightSavings()) // convert hrs to DST for display
  {
    if(rawHour==0)
      rawHour=23;
    else
      rawHour--;
  }

  Serial.print(DayOfWeek[RTCdow]); Serial.print(" ");
  Serial.print(rawHour); Serial.print(":");
  Serial.print(RTCmin); Serial.print(" ");
  Serial.print(RTCday); Serial.print("/");
  Serial.print(RTCmonth); Serial.print("/");
  Serial.print(RTCyear);
  if(isDaylightSavings()) // convert hrs to DST for display
    Serial.println(" (Daylight Savings Active)");
  else
    Serial.println("");


  uint8_t hoursTens = rawHour / 10;
  uint8_t hoursOnes = rawHour % 10;
  uint8_t minutesTens = RTCmin / 10;
  uint8_t minutesOnes = RTCmin % 10;

  // Combine digit data into a 32-bit value
  displayData |= (uint32_t)Lookup7seg[minutesOnes];
  displayData <<= 8;
  displayData |= (uint32_t)Lookup7seg[minutesTens];
  displayData <<= 8;
  displayData |= (uint32_t)Lookup7seg[hoursOnes];
  displayData <<= 8;
  displayData |= (uint32_t)Lookup7seg[hoursTens];

  // clock out data to shift registers
  for (int i = 31; i >= 0; i--) 
  {
    digitalWrite(DS_PIN, (displayData >> i) & 1);
    digitalWrite(SHCP_PIN, HIGH);
    digitalWrite(SHCP_PIN, LOW);
    delay(1);
  }

  // Latch data to outputs
  digitalWrite(STCP_PIN, HIGH);
  digitalWrite(STCP_PIN, LOW);
}

void ds1302_write(uint8_t address, uint8_t data) 
{
  digitalWrite(DS1302_CE_PIN, HIGH);
  shiftOut(DS1302_IO_PIN, DS1302_SCLK_PIN, LSBFIRST, address);
  shiftOut(DS1302_IO_PIN, DS1302_SCLK_PIN, LSBFIRST, data);
  digitalWrite(DS1302_CE_PIN, LOW);
}

uint8_t bcdToDec(uint8_t val) 
{
  return (val / 16 * 10) + (val % 16);
}

uint8_t decToBcd(uint8_t val) 
{
  return (val / 10 * 16) + (val % 10);
}

uint8_t ds1302_read(uint8_t address) 
{
  uint8_t data;
  digitalWrite(DS1302_CE_PIN, HIGH);
  shiftOut(DS1302_IO_PIN, DS1302_SCLK_PIN, LSBFIRST, address);
  pinMode(DS1302_IO_PIN, INPUT);
  data = shiftIn(DS1302_IO_PIN, DS1302_SCLK_PIN, LSBFIRST);
  pinMode(DS1302_IO_PIN, OUTPUT);
  digitalWrite(DS1302_CE_PIN, LOW);
  return data;
}

void ds1302_getTime() 
{
  ds1302_write(0x8F, 0x00); // Dummy write to DS1302 to prepare for read
  RTCsec = bcdToDec(ds1302_read(0x81) & 0x7F);
  RTCmin = bcdToDec(ds1302_read(0x83));
  RTChour = bcdToDec(ds1302_read(0x85));
  RTCday = bcdToDec(ds1302_read(0x87));
  RTCmonth = bcdToDec(ds1302_read(0x89));
  RTCdow = bcdToDec(ds1302_read(0x8b));
  RTCyear = bcdToDec(ds1302_read(0x8D));
}

void ds1302_setTime(uint8_t sec, uint8_t min, uint8_t hour, uint8_t day, uint8_t month, uint8_t year, uint8_t dow) 
{
  // no checks on data written!
  ds1302_write(0x8E, 0x00); // Disable DS1302 write protect
  ds1302_write(0x80, decToBcd(sec));   // 0..59
  ds1302_write(0x82, decToBcd(min));   // 0..59
  ds1302_write(0x84, decToBcd(hour));  // 0..23
  ds1302_write(0x86, decToBcd(day));   // 1..31
  ds1302_write(0x88, decToBcd(month)); // 1..12 
  ds1302_write(0x8A, decToBcd(dow));   // 1..7
  ds1302_write(0x8C, decToBcd(year));  // 0..99
  ds1302_write(0x8E, 0x80); // Re-enable DS1302 write protect
  ds1302_getTime();
  Serial.print(" Set Time ");
  Serial.print(RTChour); Serial.print(":");
  Serial.print(RTCmin); Serial.print(":");
  Serial.print(RTCsec); Serial.print(" - ");
  Serial.print(DayOfWeek[RTCdow]); Serial.print(" ");
  Serial.print(RTCday); Serial.print("/");
  Serial.print(RTCmonth); Serial.print("/20");
  Serial.print(RTCyear);
  if(isDaylightSavings()) // convert hrs to DST for display
    Serial.println(" (Daylight Savings Time is Active)");
  else
    Serial.println("");
}

void setup() 
{
  Serial.begin(115200); // Remember to set IDE "USB CDC ON BOOT" to ENABLED or serial WONT work!

  // INIT DS1302
  pinMode(DS1302_CE_PIN, OUTPUT);
  pinMode(DS1302_IO_PIN, OUTPUT);
  pinMode(DS1302_SCLK_PIN, OUTPUT);
  digitalWrite(DS1302_CE_PIN, HIGH);
  digitalWrite(DS1302_SCLK_PIN, LOW);

  // check clock is running and if not set it running and with a default time/date
  ds1302_write(0x8F, 0x00); // Dummy write to prepare for read
  uint8_t RTCDisabled = ds1302_read(0x81) & 0x80;
  if(RTCDisabled)
  {
    // default time
    ds1302_setTime(0, 0, 8, 1, 4, 25, 2);
  }

  // INIT shift reg
  pinMode(SHCP_PIN, OUTPUT);
  pinMode(DS_PIN, OUTPUT);
  pinMode(STCP_PIN, OUTPUT);
  pinMode(COLON_PIN, OUTPUT);
  pinMode(DSTLED_PIN, OUTPUT);
  digitalWrite(SHCP_PIN, LOW);
  digitalWrite(STCP_PIN, LOW);    

  // init WiFi
  initWiFi();

}

void loop() 
{
  ds1302_getTime();

  if(++LastUpdate>10 || LastMinVal!=RTCmin ) // Update time every 10 seconds or if minutes change (so we can see flow-active LED flash)
  {
    LastUpdate=0;
    LastMinVal=RTCmin;
    displayTime();
  }

  // flash colon at 1 sec
  if (Colon==0) Colon=1; else Colon=0;
  digitalWrite(COLON_PIN, Colon);  
  digitalWrite(DSTLED_PIN, Colon);  

  server.handleClient();  // Handle any web client requests

  delay(1000);
}

networking.h

// Colabaration between Boz and various AI's that provides the functionality to 
// connect to and set the clock using a wifi connection and web browser
// All Designs relating to this project are Public Domain.

#ifndef NETWORKING_H
#define NETWORKING_H

#include <WiFi.h>
#include <WebServer.h>

// Access Point settings
extern const char* ap_ssid;
extern const char* ap_password;
extern IPAddress local_ip;
extern IPAddress gateway;
extern IPAddress subnet;

extern uint8_t  RTCsec, RTCmin, RTChour, RTCday, RTCmonth, RTCyear, RTCdow; // global variables

extern void ds1302_setTime(uint8_t sec, uint8_t min, uint8_t hour, uint8_t day, uint8_t month, uint8_t year, uint8_t dow);
extern void displayTime(); 
extern bool isDaylightSavings();

extern WebServer server;

// Function declarations
void initWiFi();
void handleRoot();
void handleSetTime(); 
void handleNotFound();

#endif

networking.cpp

// Colabaration between Boz and various AI's that provides the functionality to 
// connect to and set the clock using a wifi connection and web browser
// All Designs relating to this project are Public Domain.

#include "networking.h"
#include <Arduino.h>

// Access Point settings
const char* ap_ssid = "Bullsi-Clock";
const char* ap_password = "boz"; // actual password not required but add one here if req
IPAddress local_ip(192, 168, 4, 1);
IPAddress gateway(192, 168, 4, 1);
IPAddress subnet(255, 255, 255, 0);

// Create web server on port 80
WebServer server(80);

void handleRoot() // handler for landing page
{
  String html = "<!DOCTYPE html><html><head>";
  html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
  html += "<style>";
  html += "body { font-family: Arial, sans-serif; text-align: center; margin: 20px; }";
  html += "h1 { color: #0066cc; font-size: 2em;}"; // Larger heading
  html += ".clock { font-size: 3em; margin: 20px 0; }"; // Larger clock
  html += "button {";
  html += "  background-color: #4CAF50;";
  html += "  color: white;";
  html += "  padding: 15px 30px;"; // Larger button
  html += "  border: none;";
  html += "  cursor: pointer;";
  html += "  font-size: 1.2em;"; // Larger font
  html += "}";
  html += "</style>";
  html += "<script>";
  html += "function sendCurrentTime() {";
  html += "  const now = new Date();";
  html += "  const hours = now.getHours();";
  html += "  const minutes = now.getMinutes();";
  html += "  const seconds = now.getSeconds();";
  html += "  const year = now.getFullYear();";
  html += "  const month = now.getMonth() + 1;";
  html += "  const day = now.getDate();";
  html += "  const dow = now.getDay() === 0 ? 7 : now.getDay();";
  html += "  const url = `/settime?hours=${hours}&minutes=${minutes}&seconds=${seconds}&year=${year}&month=${month}&day=${day}&dow=${dow}` ;";
  html += "  fetch(url);";
  html += "}";
  html += "</script>";
  html += "</head><body>";
  html += "<h1>Indevin Bulls-i Clock</h1>";
  // Display current time
  html += "<div class='clock'>";
  html += String(RTChour / 10) + String(RTChour % 10) + ":" + (RTCmin < 10 ? "0" : "") + String(RTCmin);
  html += "</div>";
  html += "<p>This clock was designed and built by Boz, the author of the original Bulls-i program which ran vintages 2008-2025.</p>";
  html += "<p>For more info on this device and what he's up to currently, goto <b>rodyne.com</b> (when you're back on a normal network!)</p>";
  html += "<p><button onclick='sendCurrentTime()'>Set Date/Time from this Phone</button></p>";
  html += "</body></html>";
  server.send(200, "text/html", html);  
}

void handleSetTime() // Handler for the settime URL (linked from the SetupWifi() fn)
{
  if (server.hasArg("hours") && server.hasArg("minutes") && server.hasArg("seconds") && server.hasArg("year") && server.hasArg("month") && server.hasArg("day") && server.hasArg("dow")) 
  {
    RTChour = server.arg("hours").toInt();
    RTCmin = server.arg("minutes").toInt();
    RTCsec = server.arg("seconds").toInt();
    RTCyear = server.arg("year").toInt() - 2000;
    RTCmonth = server.arg("month").toInt();
    RTCday = server.arg("day").toInt();
    RTCdow = server.arg("dow").toInt();
    ds1302_setTime(RTCsec, RTCmin, RTChour, RTCday, RTCmonth, RTCyear, RTCdow); // Include dow
    server.send(200, "text/plain", "Date/Time set successfully");
  } 
  else 
    server.send(400, "text/plain", "Something went wrong. (Probably your shitty phone!)");  
}

// Handler for 404 errors
void handleNotFound() 
{
  // Redirect back to the root page
  server.sendHeader("Location", "/");
  server.send(302, "text/plain", "");
}

void initWiFi() 
{
  // Disconnect from any previous WiFi connections
  WiFi.disconnect();
  // Configure access point with static IP
  WiFi.mode(WIFI_AP);
  WiFi.softAPConfig(local_ip, gateway, subnet);
  // Start the access point
  if(WiFi.softAP(ap_ssid)) 
  {
    Serial.println("WiFi Access Point started successfully");
    Serial.print("AP Name: ");
    Serial.println(ap_ssid);
    Serial.print("AP IP address: ");
    Serial.println(WiFi.softAPIP());
  } 
  else 
    Serial.println("Failed to start WiFi Access Point.");
  // Set up the web server page routes
  server.on("/", handleRoot);
  server.on("/settime", handleSetTime);
  server.onNotFound(handleNotFound);
  // Start the web server
  server.begin();
  Serial.println("Web server started");
  Serial.println("Navigate to http://192.168.4.1 in your web browser to set time");
}
]]>
2025, My Final Vintage and Open For New Business https://rodyne.com/?p=1614 Sun, 24 Nov 2024 22:25:18 +0000 https://rodyne.com/?p=1614 Continue reading →]]>

Fran, Allison and Me by the Wineries Continuous Floatation System March 2025

I developed the original Bulls-i winery management program for Indevin Ltd back in 2008 when they were just a small winery based in a single site in Blenheim, and only processing other client’s grapes into wine. Back then there were only 23 permanent staff adding 60 temporary staff during vintage (the two months of 24×7 chaos when grapes are harvested, received and processed into wine).

When I first arrived on the scene they had no IT staff, no technical people, a single Microsoft small business server supported by a local IT company and, like all startups, had a lot of growing pains. To Indevin’s credit, they learned quickly from their mistakes, and I usually reacted immediately to change the program to support them.

Today Indevin is the biggest wine company in New Zealand with four very different wineries including Villa Maria, and a dozen offices in both NZ and Australia. The original requirements of the program I wrote changed at least a dozen times, and far beyond its original requirements; At the start it did all its core winery duties of managing the grape and wine processing from receival to dispatch, and within a year or two we had added sales, purchasing, QA, maintenance, vineyard management and even at one point the payments for the vineyard contractors.

The IT (technology) department of Indevin now consists of twenty-seven people (and growing!), three programmers, and the rest split between business analysts, hardware support people and managers of various types. Their movement from a small/medium business into a corporate behemoth moved much quicker than at any previous company I have been involved with, and this, at times has been uncomfortable, with change requests even coming in on live production systems with trucks waiting to unload, and multiple late-night callouts. Plus, the two best months of the year I am almost always at one of their offices!

With the split-off of the core program sales, purchases, health and safety, maintenance, HR, viticulture, etc. systems, the IT department have taken the low-hanging fruit and, as of today Nov 2024, about 50% finished the program for the core winemaking. I have been supportive of these changes and have been helping wherever possible, because as a single developer I am really struggling to support all the sites and interfaces with the 24×7, almost immediate response time they demand. Also, since starting on this journey, Indevin has taken so much of my time I have not been able to support many of the open-source projects I first envisaged when I started the company.

So, hopefully without jinxing anything, 2025 will be my last vintage.

End of July 2025 will see the company replace the core winemaking parts of the systems and transfer the data over. The remainder of that year they will develop the front-end systems required to interface to the receival equipment for vintage which will require minimal consultation input from me. This means that after a holiday to the UK to visit family and friends, I will once again be available for other projects.

I doubt I will ever get on such a wild ride again, I am quite looking forward to being able to learn some new skills such as with the new LLM and AI models, and, assuming AI does not replace what I can do, working with other companies in the same position doing hands-on projects in slightly less chaotic environments.

]]>