Microtransat - still alive ? & 2026 Race

56 views
Skip to first unread message

Robin Lovelock on robin@gpss.co.uk

unread,
Apr 12, 2026, 6:57:11 AMApr 12
to Colin Sauze' via Microtransat Microtransat
Hi Guys. Is Microtransat and it's web site(s) still alive ?
I wanted Snoopy to appear on the 2026 enties as seen on http://www.gpss.co.uk/autop.htm 
with the only difference is the "John's site" link can be removed - no longer exists.

When I followed links, such as that to Microtransat at the top of "Snoopy" above
-> http://www.microtransat.org/?lang=en I got problems with a Chrome browser
- maybe the (American) https has been revoked or not been renewed ?
I still use http for most of my sites - NOTHING is secure on the Net ;-)

No change in work needed (by someone) on Snoopy's boat. e.g. spares for computer ;-)
I thought the above might take time for someone to fix, as was done well several years ago ;-)

Take Care
Robin ( & Snoopy )

James

unread,
Apr 12, 2026, 10:17:02 AMApr 12
to Microtransat
I echo Robin's interest in knowing if the MicroTransat Challenge is, in his words, "alive."  It is now April 2026 and the MicroTransat website still reflects 2025 data and, in retrospect, last year it remained outdated for most of that year as well.  For those of us that are keen to participate in the Challenge (or even just monitor a particular boat's progress) this is quite disappointing.

There are MANY reasons why the MicroTransat Challenge must remain "alive" and actively showcase such.  Two reasons that come to the top of my head are 1) as a venue for educational institutions (high school and college teams) to advance students entering the various technology fields and 2) as a venue for individual hobbyists who seek success in overcoming very difficult problems.  I fall into the latter category but have provided input to a local university's engineering department regarding the former and how they could use the Challenge as a design team participant.  Both cases rely upon the success and continuation of MicroTransat organization.

Separately, I am a member of a local amateur radio club and have twice been asked to deliver a presentation that outlines my efforts to incorporate an amateur radio beacon transmitter aboard my future Challenge entry.  In each case I have been asked what reward I expect to receive if I were to successfully complete the MicroTransat voyage.  In each case I responded with merely being able to have "bragging rights" amongst my friends because that is, in reality, the only reward any one of us might expect.  While not necessary, it would be best if such an accomplishment were to be achieved within the context and oversight of a recognized governing body, i.e., the MicroTransat Challenge organization.

Conversely, what I think the MicroTransat Challenge organization does not want to have happen is for some very large corporate entity to "take over" the idea of the competition and run their own.  While I highly regard Elon Musk and all of his accomplishments -especially with regard to autonomously driven Tesla cars- I prefer the rather cosy and collaborative perspective that fellow MicroTransat participants have already fostered.  Let's keep it that way by ensuring that the MicroTransat Challenge's online presence is updated and that collaborative competition is showcased.

Just my two cents worth of opinion.

James Hull
Norfolk, Virginia, USA

Damon McMillan

unread,
Apr 13, 2026, 12:25:19 PMApr 13
to James, Microtransat
I agree, I would love to see Microtransat - or something like it - not only survive but flourish.  My company, Blue Trail Engineering, would be willing to put up some prize money for any team that can make it across the Atlantic (although only for amateurs, not for companies).  I think if there were a few other companies or individuals putting up prize money, that might attract more participants.

While I totally agree with this paragraph...

Conversely, what I think the MicroTransat Challenge organization does not want to have happen is for some very large corporate entity to "take over" the idea of the competition and run their own.  While I highly regard Elon Musk and all of his accomplishments -especially with regard to autonomously driven Tesla cars- I prefer the rather cosy and collaborative perspective that fellow MicroTransat participants have already fostered.  Let's keep it that way by ensuring that the MicroTransat Challenge's online presence is updated and that collaborative competition is showcased.

... I also believe that there needs to be a bit more marketing, maybe in the form of a flashy website and some outreach effort, especially to attract the attention of university teams.  By no means is that a criticism of the awesome work that others have done on Microtransat!  You have done far better than I could.  I'm just trying to think of some ingredients that might be missing.

Damon

Damon McMillan
Blue Trail Engineering LLC
1567 Skyway Dr. Unit B
Longmont, CO 80504
720-526-6388 (business)
www.bluetrailengineering.com



--
You received this message because you are subscribed to the Google Groups "Microtransat" group.
To unsubscribe from this group and stop receiving emails from it, send an email to microtransat...@googlegroups.com.
To view this discussion, visit https://groups.google.com/d/msgid/microtransat/33cd031b-28f4-40d0-b8e7-5e21adf76588n%40googlegroups.com.

Alastair Lidstone

unread,
Apr 13, 2026, 2:42:47 PMApr 13
to Damon McMillan, James, Microtransat
Hi All,
Great to hear there is still interest in the microtransat! It is still up and running but unfortunately there’s been very little in the way of entries recently. 

The microtransat is being managed by Colin plus 3 volunteers so thats the reason why responses are slow and the website is not always up to date! We do try our best but we’re all busy with our day to day priorities. 

@Robin - we will add Snoopy to the 2026 list of entrants! Any idea when you plan to launch? 

Are there any other builds that are close to completion? Keen to hear from anyone who is working on something! 

@Damon - are you never tempted to make an attempt with your solar boat? You’ve made some impressive attempts across the pacific. The Atlantic must be possible 😀. 

I’ll let Colin reply on the sponsorship part.

Thanks
Alastair


On 13 Apr 2026, at 18:25, Damon McMillan <da...@bluetrailengineering.com> wrote:



James

unread,
Apr 13, 2026, 3:39:24 PMApr 13
to Alastair Lidstone, Damon McMillan, Microtransat
Alastair, et al.:

That is fantastic news, thanks for letting us know.

You wanted to know if anyone else was working toward an entry.  Like probably several others on this e-mail chain, I have been developmentally working toward a MicroTransat Challenge entry but am not yet ready to attempt a crossing.  I have built three separate boats to “prove out” my boat’s unique sail concept and for writing (from scratch) the navigation algorithm that will control the boat but I will not build a TransAtlantic version until the 2026-2027 winter.  Thus, I’m looking to be a 2027 entry.

For those that are interested, last year’s attempt by the PassAt team seems to have adopted the exact same sail design concept as I proposed a couple of years ago.  Attached is a photo of my workshop with the ’stable’ of my three boats on display and you should be able to identify the similarity.  I have done extensive solar power modeling and testing to verify that while the largest of my three boats can sail continuously, the boat I will sail across the Atlantic will have more redundancy and capacity enabling it to recover from expected periods of diminished solar power generation.  Short-range riverine field testing of my navigation program has been very successful and my computer simulation of a TransAtlantic attempt was also consistently successful.

As noted below, one of the major additions that I plan on including aboard my boat is an amateur radio beacon transmitter.  This will provide redundancy for notifying the MicroTransat officials of the boat’s progress, SATCOM as primary and amateur radio as secondary.  It will also serve as a rather interesting radio application that the “ham” community might like to hear about as well.  As you might be able to discern in the attached photo’s background, I am toying with the idea of using a “magnetic loop” antenna as it is quite efficient at low antenna heights -a significant limitation on such small boats as ours.  I may, however, decide to use a more traditional vertical “whip” antenna for the boat.  TBD.

That, in a nutshell, is a brief status of my efforts to date.  More can be gleaned from YouTube videos I posted a while back and I invite your comments: https://www.youtube.com/watch?v=xhs0LwTlmZc

James Hull
Norfolk, Virginia, USA

Protoypes 1- 2-3.jpeg

Robin Lovelock on robin@gpss.co.uk

unread,
Apr 13, 2026, 4:14:58 PMApr 13
to Damon McMillan, Alastair Lidstone, James, Microtransat
Hi Guys. Great News, and those Microtransat pages are now all working for me ;-)

Snoopy's Plans for 2026 ? No change from that public for last year or two
on "Snoopy" page http://www.gpss.co.uk/autop.htm - things like spares for the computer.
BUT progress on Microtransat website means I'ii "chase up" those who might help ;-)

Great if "Snoopy" appears on a new 2026 page of entries.

One thing I could have mentioned years ago: under "media copverage" near fron to Microtransat site, I would suggest that live BBCTV coverage of Snoopy's first 2012 Attempt: https://www.youtube.com/embed/o2X6mfyUTLM

When "Colin" was mentioned, I hope that was Colin Sauze - excellent wrk over the years, including setting up the Google Group.

Thanks all.

Robin Lovelock on robin@gpss.co.uk

unread,
Apr 13, 2026, 4:33:51 PMApr 13
to Damon McMillan, Alastair Lidstone, James, Microtransat
Hi All. First a correction to that "Snoopy" page link: http://www.gpss.co.uk/autop.htm 

No need for Damon to email me seperately - I prefer just to get these Google Group emails.

and, if you include that 2012 BBC TV News video, the more detailed version
is on the "Snoopy" page above and this extract MAY work (image missing?) >>>

Play the BBC November 2012 video of Snoopy's launch on BBC TV South News with Robin getting soaked. Then family video of Snoopy's 2012 launch with BBC reporters too :-)
<<<Snoopy's first Atlantic Attempt was in 2012 ...
BBC TV South broadcast of launch, reported by Ben Moore, BBC Reporter
On Monday, April 13, 2026 at 09:14:59 PM GMT+1, 'Robin Lovelock on ro...@gpss.co.uk' via Microtransat <microt...@googlegroups.com> wrote:BBC TV South broadcast of launch, reported by Ben Moore, BBC Reporter



Francis ROUSSEL

unread,
Apr 14, 2026, 3:26:41 AMApr 14
to microt...@googlegroups.com
Bonjour à tous,

Heureux de voir qu'une petite communauté s'intéresse au concept de
voilier autonome.

Voici quelques unes de mes réflexions

- Le monde de ceux qui ont ou veulent tenter la Microtransat est
principalement composé de personnes ou groupes sachant programmer.
- En temps de renchérissement du coût des transport les déplacements
pour une mise à l'eau sont difficiles tant pour la Microtransat que pour
la MicroSel.
- Je souscris au fait que c'est une concurrence impossible à soutenir si
on a des professionnels qui participent.
- Même si on a sur internet le projet Ardupilot voilier, on a aucun
bateau qui s'est équipé avec.
- Devoir transmettre sa position a un coût élevé : achat d'un
émetteur-récepteur satellite. Encore plus pour microSel avec
l'obligation d'un système AIS, bien que j'adore l'idée de devoir
transporter 1 kg de sel. En revanche les organisateurs ont oublié la
réglementation qui veut qu'à partir de 2.5m hors tout on a un navire
avec les obligations qui en découlent. Limiter à moins de 2;5m aurait
été plus adapté.
- Les participants ont très peu de connaissances du monde des voiliers
radiocommandés, soit ils dessinent une coque, très loin des standards de
la voile radio-commandée soit utilisent une coque existante issue de ce
monde. Et les modèles de la voile radiocommandée sont adaptés à des
plans d'eau même très ventés mais pas pour un océan, l'adaptation au
vent se faisant par changement de la voilure.
- J'ai tenté d'intéresser plusieurs écoles d'ingénieurs ou universités
au projet sans succès, tantôt c'est la voie informatique qui existe mais
pas celle de la construction et inversement. J'ai proposé à une division
universitaire qui avait déjà une sorte de challenge pour obtenir le
voilier d'environ 2,3m le plus rapide en régate radiocommandée de
s'associer à la division informatique, l'une en réservant une place
définie pour un équipement et l'autre en produisant l'équipement, les
prés carrés de chacune ont tué le projet dans l’œuf.
- Dans le monde de la voile radiocommandée on a eu de beaux jours pour
la création de voiliers mais avec peu de participants. Le nombre à
explosé avec l'arrivée de voiliers commerciaux tout faits et avec une
impossibilité de modifier le voilier. es propriétaires voulant d'abord
régater et pas passer leur temps à construire, ceux qui au contraire
voulaient construire restant dans le monde de la maquette.
- Les équipes universitaires sont difficiles à mettre en place, les
étudiants passent que quelques années , souvent une seule sur le projet,
l'existence est liée au bon vouloir de l'équipe enseignante.

Pourquoi ne pas mutualiser une partie de nos recherches afin
d'intéresser une partie des modélistes voile à organiser de petites
rencontres de voiliers autonomes en définissant une équipement minimum
permettant à un participant de démarrer, un peu comme au siècle dernier
la venue des radiocommandes super superhétérodynes a permis de régater à
plusieurs en navigant ensemble.

Que faudrait-il ?
- un équipement radiocommande 2 servos : gouvernail et voiles
- un microcontrôleur avec des entrées venant du récepteur de
radiocommande pour assurer la récupération en cas de problème
- un gps,
- un lecteur de carte SD pour enregistrer la trace afin de publier
celle-ci avec fourniture au départ d'une carte SD avec les waypoints
définis pour tous.
- j'ignore si un compas est nécessaire
- le programme de base à mettre dans le microcontrôleur pour démarrer.

2 catégories : équipement standard côté informatique, et équipement
libre. sous catégories avec les classes de modèles réduit: Tenrater,
IOM, M, RG65.

Qu'en pensez-vous ? qui est prêt à fournir d'abord un organigramme du
programme, puis un fichier minimum ?


Cordialement

fr
Hello everyone,

I'm happy to see that a small community is interested in the concept of
autonomous sailboats.

Here are some of my thoughts:

- The world of those who have completed or want to attempt the
Microtransat is mainly composed of individuals or groups who know how to
program.

- With rising transportation costs, getting to the launch site is
difficult for both the Microtransat and the MicroSel.

- I agree that it's impossible to sustain this competition if
professionals are participating.

- Even though the Ardupilot sailboat project is available online, no
boats have been equipped with it.

- Transmitting your position is expensive: you have to buy a satellite
transceiver. This is even more costly for the MicroSel with the
requirement of an AIS system, although I love the idea of ​​having to
carry 1 kg of salt. However, the organizers overlooked the regulations
stipulating that anything over 2.5 meters in overall length constitutes
a vessel, with all the associated obligations. Limiting it to less than
2.5 meters would have been more appropriate.

- The participants have very little knowledge of the world of
radio-controlled sailboats. They either design a hull, which is far
removed from radio-controlled sailing standards, or they use an existing
hull from that world. Radio-controlled sailboat models are designed for
even very windy bodies of water, but not for the ocean, where wind
adaptation is achieved by changing the sails.

- I tried to interest several engineering schools and universities in
the project without success. Sometimes the computer science track
exists, but not the construction track, and vice versa. I proposed to a
university department, which already had a challenge to build the
fastest radio-controlled sailboat (approximately 2.3 meters) for
regattas, that they partner with the computer science department. One
department would allocate a specific space for a piece of equipment, and
the other would produce the equipment. However, their respective areas
of expertise ultimately killed the project before it even began.

- In the world of radio-controlled sailing, there was a golden age for
sailboat design, but with few participants. The number exploded with the
arrival of ready-made commercial sailboats, which were impossible to
modify. Owners wanted to race first and not spend their time building,
while those who did want to build remained in the world of model making.

- University teams are difficult to organize; students only spend a few
years, often just one, on the project, and its existence depends on the
goodwill of the teaching staff.

Why not pool some of our research to encourage model sailboat
enthusiasts to organize small gatherings of autonomous sailboats? We
could define a minimum equipment requirement for a participant to get
started, much like how the advent of superheterodyne radio transmitters
in the last century allowed several people to race together.

What would be needed?

- A radio transmitter with two servos: rudder and sails
- A microcontroller with inputs from the radio transmitter receiver to
ensure recovery in case of problems
- A GPS
- An SD card reader to record the track for publication, with an SD card
containing the waypoints defined for everyone to use.

- I don't know if a compass is necessary
- The basic program to install on the microcontroller to get started.

Two categories: standard computer equipment, and optional equipment.
Subcategories based on model classes: Tenrater, IOM, M, RG65.

What do you think? Who is willing to provide a program flowchart first,
then a minimum file?

Regards

fr

--
Cet e-mail a été vérifié par le logiciel antivirus d'Avast.
www.avast.com

James

unread,
Apr 14, 2026, 9:13:11 AMApr 14
to Microtransat
Francis:

You make some interesting points in your e-mail above.  Please let me amplify some of your points:

Over the past few decades, model construction of any type, which was a robust hobby in the 50s, 60s and 70s, has given way to the ubiquity of “almost ready to fly / float” commercial products.  That has has, in turn, almost eliminated the average hobbyist’s interest to scratch build boats or airplanes and with it the general loss of knowledge, skills and abilities needed for custom projects.

Trans-oceanic autonomous sailboat conception, design, construction and operation requires an extremely broad array of expertise.  Few teams have the necessary knowledge, skills and abilities to complete this kind of complex project.  Fewer individuals are able to do so.

There are no off-the-shelf boats, control systems or navigation programs to leverage for this kind of project.  Everything, from boat building to wiring to programming, is custom work especially in light of the extreme environment for a trans-oceanic voyage.

One-off custom boatbuilding is expensive.  Electronic parts required to fabricate a programmable control system are not as expensive to purchase but the time required for an individual to author a one-off custom navigation algorithm is very expensive time-wise and skill-wise.  The rise of Artificial Intelligence platforms capable of custom developing a navigation program is probably on the near-term horizon so that may accelerate that process and mitigate that cost.  Physical construction, however, not so much.

But in the end every MicroTransat Challenge entrant will shove their tiny boat into the enormous Atlantic Ocean and watch it sail away… and with it the builder's investment of much time and money… in the hope that it survives to achieve it’s pre-programmed distant goal.  In reality, however, the personal satisfaction is probably derived more from the years-long journey in building such a craft and creating such an intelligent navigation system and not by the final destination found by a GPS.

But being the first in the world to do so sounds pretty cool, too.


James Hull
Norfolk, Virginia, USA

Robin Lovelock on robin@gpss.co.uk

unread,
Apr 14, 2026, 9:44:42 AMApr 14
to Microtransat, James
Thanks Francis & James. Wise words from both of you, BUT 4 years of prototyping of Snoopy's boats
from 2008 to 2011 resulted in our adopting standard Marblehead 1.2 metre long fibreglass hulls & keels.

The good news is that the guys who once used these, in local model boat sailing clubs, end up "popping their clogs"
or giving up the hobby - resulting in those hulls being given way or sold VERY cheaply.

I have several spare here - BUT, over the years, we got almost all the boats back
- with (alleged) help from the UK Royal Marines; e.g. 2015 ;-)

We never did get that 2012 Snoopy Snoopy back, but did get emails from Kabul
saying things like "Do you want your boat back ? :-)"
I like to think it is somewhere like a military naval college or maybe the "mess" at Poole ;-)

Some guys enjoy the "work" of building, but in Defence Systems, I was always a "Buy not build" guy ;-)
My enjoyment comes in making progress, and seeing the result of (other's) hard work ;-)

Take Care - and don't get caught ;-)
Robin
p.s. if you track me on that public black SOS tracker, you may see us going to take stuff to the waste dump
near Maidenhead, and - MAYBE - to places like Bray Lake - to see if anything has changed ;-)

--
You received this message because you are subscribed to the Google Groups "Microtransat" group.
To unsubscribe from this group and stop receiving emails from it, send an email to microtransat...@googlegroups.com.

Francis Roussel

unread,
Apr 14, 2026, 11:27:17 AMApr 14
to Microtransat
Bonjour James,

On est donc bien d'accord sur les constats et je suis entièrement d'accord sur le temps de développement des programmes.
J'ai testé l'usage de Chatgpt et c'est vrai que dans certains points cela permet d'éviter pour un non professionnel du codage de longues recherches sur internet pour résoudre un problème.
Mais il ne faut pas se leurrer, l'IA ne fait que recracher ce qu'elle a lu quelque part et lui demander de fournir un programme complet de navigation est impossible à l'heure actuelle.
Lui demander comment coder la transmission d'un signal RC reçu et sa sortie ou pas : oui ; fournir le code pour lire et écrire sur une cartouche SD aussi, lire un mpu9250 sera fourni mais pas cracher le code pour un compas capable de se réinitialiser en mer avec une coque remuée par le houle.
Définir une route avec virements de bords en remontée vers le vent dans un couloir défini peut-être si elle a lu les archives du groupe mais vu le trafic de celui-ci j'en doute.
Après demander l'assemblage complet c'est pour un futur trop loin pour moi.
Pleins de projets "libres" existent dans beaucoup de domaines et souvent des participants agissent juste pour faire avancer celui-ci sans pour autant en avoir réellement besoin, le tout est de commencer par mettre un bout de code sur github.
La boîte noire dont j'imagine le futur peut simplement être un accessoire à placer entre le récepteur Rc et les servos, des régates avec prêt de cette boîte  ne reviendrait pas à très cher et pourrait être rentabiliser contre une participation modique. On pourrait aussi s'adresser aux bateaux électriques. 
Je participe à des régates en voilier réel monoplace ou biplace et certaines d'entre elles sont "flotte collective", le club organisateur fournit un bateau contre une modique somme (un peu plus qu'une place de cinéma) et le club revit, les bateaux sont tous attribués à chaque fois. Ce qui n'empêche pas certains d'acheter des bateaux  anciens, de les remettre en état et de venir régater avec, voire d'acheter du neuf. Des séries anciennes  que la mode à oublier permettent ainsi de revivre au moins localement parce que cette "flotte collective" existe.
S'amuser tout seul dans son coin c'est bien surtout pour les adolescents ;-)
Le "c'est moi qui l'a fait " aussi
Mais quel plaisir on a en groupe, la preuve c'est qu'on forme un groupe nous aussi
fr


Hello James,

So we agree on the observations, and I completely agree about the program development time.

I tested using Chatgpt, and it's true that in some ways it saves a non-professional coder from lengthy internet searches to solve a problem.

But let's not kid ourselves, the AI ​​only regurgitates what it has read somewhere, and asking it to provide a complete navigation program is currently impossible. Asking it how to code the transmission of a received RC signal and whether or not to output it: yes; providing the code to read and write to an SD card: yes; reading an MPU9250 will be provided, but not generating the code for a compass capable of resetting itself at sea with a hull tossed by swell. Defining a route with upwind tacks in a defined corridor: perhaps, if it has read the group's archives, but given the group's traffic, I doubt it.

After requesting the complete assembly, that's too far off for me in the future.

Plenty of "open source" projects exist in many fields, and often participants contribute simply to advance them without necessarily needing it. The key is to start by putting a piece of code on GitHub.
The black box I envision could simply be an accessory placed between the RC receiver and the servos. Regattas with this box available for loan wouldn't be very expensive and could be profitable with a small entry fee. We could also apply this to electric boats.
I participate in regattas with real single- or double-handed sailboats, and some of them are "shared fleet" regattas. The organizing club provides a boat for a small fee (a little more than a movie ticket), and the club thrives, as all the boats are allocated each time. This doesn't stop some people from buying old boats, restoring them, and racing with them, or even buying new ones. Old series that have fallen out of fashion are thus being revived, at least locally, because this "collective fleet" exists. Having fun on your own is fine, especially for teenagers ;-) The "I made it myself" attitude is great too.

But what fun we have in a group! The proof is that we're forming a group ourselves.

fr

Sean Fish (GTMRG)

unread,
Apr 14, 2026, 2:05:50 PMApr 14
to Microtransat
Hi Francis and James,

I agree that the decline of hobby R/C has made it challenging both to capture attention as well as get started with these sorts of challenges. 
R/C sailing itself is very niche within the hobby, compared to R/C planes and multirotors. I don't think RTF can really be to blame, the hobby in general is not what it used to be - and if you look at the multirotor community, there's still a lot of interest in building quads for racing and FPV.

I do believe there is interest out there. YouTube projects for R/C submarines and surface vehicles seem to be having a bit of a resurgence, if you look at channels such as rctestflight and ProjectAir.
It would definitely be helpful to have some kind of introductory educational material out there for getting started with R/C sailing, akin to what FliteTest provided for the R/C flying community.
Something based on the standard mini classifications from 3dprintedradioyachts would maybe be a good starting point for new people (sadly the website appears to have gone offline in the last few months but it's accessible via the Internet Archive).

Our team has definitely required a lot of runway to get started with the challenge, with a vehicle almost ready for water testing after about 4 years, based loosely off of the MaxiMOOP. 

Best,
Sean

Damon McMillan

unread,
Apr 15, 2026, 12:02:22 PMApr 15
to Alastair Lidstone, James, Microtransat
Hi Alastair,

Totally understood about having other responsibilities.  I'm in the same boat; I'm sure all of us are.

The only reason I launched my solar boats from California is that my parents live there.  Oh, and because going to rescue a boat if it has to divert to Hawaii is a pleasant task!  Besides, isn't the north Atlantic generally less sunny?  Solar boats don't do well with clouds.

I admire all of you who make sailboats work.  I have a goal to try that at some point.  

Damon

Damon McMillan
Blue Trail Engineering LLC
1567 Skyway Dr. Unit B
Longmont, CO 80504
720-526-6388 (business)
www.bluetrailengineering.com


Alec Adams

unread,
Apr 16, 2026, 4:00:19 AMApr 16
to Microtransat
Hello Alec and Shirley here. We have almost finished a program for our boat using AI. Neither of us have coded before but we did take a months course on the internet on coding before starting this venture and it's taken months and it is still not finished as we have to add feedback for the rudder and mast position .  Not sure about making it public on GitHub yet but we can do a text version of what we have so far. At the moment it has an SD card and an Oled on it for testing. We also have a  flowery notebookLM description of what this code does https://notebooklm.google.com/notebook/d5379604-a77b-4666-a839-7f20a2e9dadc. This is the code any suggestions or ideas, we would be grateful for.  Repository: alfromuk/mighty.git
Files analyzed: 21

Estimated tokens: 11.4k

Directory structure:
└── alfromuk-mighty.git/
    ├── platformio.ini
    ├── include/
    │   ├── BoatController.h
    │   ├── CompassManager.h
    │   ├── DisplayManager.h
    │   ├── GPSManager.h
    │   ├── NavigationManager.h
    │   ├── PinConfig.h
    │   ├── RudderController.h
    │   ├── SailController.h
    │   ├── SDLogger.h
    │   └── WindSensor.h
    └── src/
        ├── BoatController.cpp
        ├── CompassManager.cpp
        ├── DisplayManager.cpp
        ├── GPSManager.cpp
        ├── main.cpp
        ├── NavigationManager.cpp
        ├── RudderController.cpp
        ├── SailController.cpp
        ├── SDLogger.cpp
        └── WindSensor.cpp


================================================
FILE: platformio.ini
================================================
; PlatformIO Project Configuration File
;
;   Build options: build flags, source filter
;   Upload options: custom upload port, speed and extra flags
;   Library options: dependencies, extra library storages
;   Advanced options: extra scripting
;
; Please visit documentation for the other options and examples
; https://docs.platformio.org/page/projectconf.html

[env:esp32dev]
platform = espressif32
board = esp32dev
framework = arduino
monitor_speed = 115200
lib_deps =
mikalhart/TinyGPSPlus@^1.0.3
madhephaestus/ESP32Encoder@^0.10.1
sparkfun/SparkFun BNO08x Cortex Based IMU@^1.0.6
davidarmstrong/WMM_Tinier@^1.0.3
adafruit/Adafruit SSD1306@^2.5.16
adafruit/Adafruit GFX Library@^1.12.5



================================================
FILE: include/BoatController.h
================================================
#pragma once

#include "GPSManager.h"
#include "CompassManager.h"
#include "WindSensor.h"
#include "NavigationManager.h"
#include "RudderController.h"
#include "SailController.h"
#include "DisplayManager.h"
#include "SDLogger.h"
#include <Preferences.h>
#include <LittleFS.h>
#include <freertos/FreeRTOS.h>
#include <freertos/semphr.h>
#include <freertos/task.h>

class BoatController {
public:
    // Core Functions
    void begin();
    void printTasks();
     CompassManager compass;
     NavigationManager nav;
    // Mission Loader for the Field Walk
    void loadFieldTestMission();

private:
    // Core Managers
    GPSManager gps;
    //CompassManager compass;
    WindSensor wind;
    //NavigationManager nav;
   
    // Hardware Controllers
    RudderController rudder;
    SailController sail;

    // Peripherals
    DisplayManager display;
    SDLogger sdLogger;

    // Timing and State
    Preferences preferences;
    int lastMinute;
    int lastMotorMoveMinute;    // Tracks the last time the worm-drive was triggered
    long timeOffset;
    unsigned long lastSyncMillis;
    unsigned long lastLogTime;

    // Decision Averaging Buckets
    float headingErrorAccumulator; // Sum of errors over the 30-sec/5-min window
    int errorSampleCount;         // Number of GPS samples collected

    // Persistence and Internal Logic
    void syncTime();
    void saveState();
    void loadState();
    void logData();

    // FreeRTOS Mutex and Tasks
    SemaphoreHandle_t navMutex;
    TaskHandle_t navigatorTaskHandle;
    TaskHandle_t pilotTaskHandle;

    static void navigatorTask(void* pvParameters);
    static void pilotTask(void* pvParameters);
};



================================================
FILE: include/CompassManager.h
================================================
#ifndef COMPASSMANAGER_H
#define COMPASSMANAGER_H

#include <SparkFun_BNO08x_Arduino_Library.h>
#include <Arduino.h>

struct Quaternion {
    float i, j, k, real;
};

class CompassManager {
public:
    CompassManager();
    void begin();
    void update();
    //void startCalibration();
   
    float getHeadingTrue() const;
    float getPitch() const;
    float getRoll() const;
    bool isHardwareOK() const;

    // --- NEW FOR FIELD TESTING ---
    void startCalibration();         // Enables the calibration engine
    void saveCalibration();          // Saves offsets to BNO085 Flash
    uint8_t getCalibrationStatus();  // Returns 0 (Unreliable) to 3 (High)

private:
    BNO08x myIMU;
    Quaternion quat;
    bool hardwareStatus = false;
    float declination = -1.0f; // Adjust based on your local magnetic north
};

#endif


================================================
FILE: include/DisplayManager.h
================================================
#ifndef DISPLAYMANAGER_H
#define DISPLAYMANAGER_H

#include <Arduino.h>
#include <Wire.h>
#include <Adafruit_GFX.h>
#include <Adafruit_SSD1306.h>

#define SCREEN_WIDTH 128
#define SCREEN_HEIGHT 64

class DisplayManager {
public:
    DisplayManager();
    void begin();

    // Updates display with navigation and motor data
    // xte is cross track error (distance).
    void update(double xte, float distance, bool rightOfLine, float rudderDeg, int rudderDir, float sailDeg, int sailDir);

private:
    Adafruit_SSD1306 display;
    bool initialized;
};

#endif



================================================
FILE: include/GPSManager.h
================================================
#ifndef GPSMANAGER_H
#define GPSMANAGER_H

#include <TinyGPSPlus.h>

struct GPSData {
    double latitude;
    double longitude;
    float speedKmph;
    float courseDeg;
    bool valid;
};

class GPSManager {
public:
    void begin(HardwareSerial &serialPort, uint32_t baud = 9600);
    void update();
    GPSData getData();
    double courseTo(double targetLat, double targetLon);
    double distanceTo(double targetLat, double targetLon);

    TinyGPSPlus& getRawGPS() { return gps; }

private:
    TinyGPSPlus gps;
    HardwareSerial* gpsSerial;
};

#endif



================================================
FILE: include/NavigationManager.h
================================================
#pragma once
#include "GPSManager.h"
#include <math.h>

class NavigationManager {
public:
    static constexpr int maxWaypoints = 20;
    static constexpr float WAYPOINT_RADIUS_METERS = 5.0f;

    NavigationManager();
    void begin();
   
    // The main brain: processes GPS, Heading, and Wind
    void update(const GPSData& pos, float heading, float windAngle);
   
    // Getters for your Motor/Laptop logging
    float getHeadingError() const;
    double getXTE() const;
    bool isInIrons() const;
   
    // Waypoint Logic
    bool isAtWaypoint(const GPSData& pos) const;
    bool hasNextWaypoint() const;
    void advanceWaypoint(const GPSData& pos); // Updated to take pos to set new 'start'
   
    // Storage & Setup
    void resetWaypoints();
    void addWaypoint(double lat, double lon);
    void saveWaypoints();
    void loadWaypoints();
    void printWaypoints() const;

private:
    // Internal Math Helpers
    double distanceTo(double lat1, double lon1, double lat2, double lon2) const;
    double calculateXTE(double sLat, double sLon, double tLat, double tLon, double pLat, double pLon);
    double toRadians(double degree);

    // Waypoint Storage
    double latitudes[maxWaypoints];
    double longitudes[maxWaypoints];
    int waypointCount;
    int currentIndex;

    // The "Rail" (XTE) Navigation
    double startLat, startLon; // Coordinates where the current leg began
    double xte;                // Distance from the rail in meters

    // Steering & Environment
    float headingError;
    bool inIrons;
    float lastWindRelative;    // For hysteresis
    bool firstLock;            // To initialize the very first 'start' point
};


================================================
FILE: include/PinConfig.h
================================================
#ifndef PINCONFIG_H
#define PINCONFIG_H

#include <Arduino.h>

// ============================================================
// 1. SPI BUS (BNO085 IMU)
// ============================================================
// Standard ESP32 SPI Pins: SCK (18), MISO (19), MOSI (23)
// These three additional pins are required for the BNO085:
#define BNO_CS          5   // Chip Select
#define BNO_INT         4   // Interrupt (Tells ESP32 data is ready)
#define BNO_RST         2   // Reset (Allows ESP32 to reboot the sensor)

// ============================================================
// 1.5 SD CARD & DISPLAY
// ============================================================
#define SD_CS_PIN       15  // SD Card Chip Select
#define OLED_SDA        21  // Display I2C SDA
#define OLED_SCL        22  // Display I2C SCL

// ============================================================
// 2. SERIAL PORTS (GPS)
// ============================================================
// Using Serial2 for the GPS Neo-6M
#define GPS_RX_PIN      16  // ESP32 RX2 <--- Connect to GPS TX
#define GPS_TX_PIN      17  // ESP32 TX2 <--- Connect to GPS RX

// ============================================================
// 3. MOTOR CONFIGURATION (LEDC PWM)
// ============================================================
#define PWM_FREQ_MOTOR  20000 // 20 kHz for silent operation
#define PWM_RESOLUTION  8     // 8-bit (0-255)

// --- Rudder Motor (Channel 0) ---
#define RUDDER_PWM_PIN  25    
#define RUDDER_IN1_PIN  26    
#define RUDDER_IN2_PIN  27    
#define RUDDER_STBY_PIN 14

// --- Sail Motor (Channel 1) ---
#define SAIL_PWM_PIN    32    
#define SAIL_IN1_PIN    33    
#define SAIL_IN2_PIN    13    
#define SAIL_STBY_PIN   14    // Changed from 4 to 14 to avoid BNO_INT conflict

// ============================================================
// 4. WIND SENSOR (Rotary Encoder)
// ============================================================
// Note: GPIO 34 and 35 are input-only.
// Use external 10k pull-up resistors to 3.3V on these pins.
#define WIND_SENSOR_PIN_A 34
#define WIND_SENSOR_PIN_B 35

// ============================================================
// 5. STATUS LED
// ============================================================
#define STATUS_LED_PIN    12  // Useful for "Heartbeat" or Watchdog status

#endif



================================================
FILE: include/RudderController.h
================================================
#ifndef RUDDERCONTROLLER_H
#define RUDDERCONTROLLER_H

#include <Arduino.h>

class RudderController
{
public:
    void begin(uint8_t in1Pin, uint8_t in2Pin, uint8_t pwmPin, uint8_t stbyPin);

    // Updates the rudder based on heading error (-180 to 180)
    void update(float headingError);

    void escapeIrons();
    void stop();

    // For logging/display
    float currentDeg = 0.0f;
    int currentDir = 0;

private:
    unsigned long lastUpdateTime = 0;
    uint8_t pinIn1, pinIn2, pinPWM, pinStby;
    const uint8_t ledcChannel = 0;

    // PID Constants
    float Kp = 2.0; // Proportional gain (e.g., 4 * 45deg error = 180 PWM)

    void applyPWM(int pwm, int direction);
};

#endif



================================================
FILE: include/SailController.h
================================================
#ifndef SAILCONTROLLER_H
#define SAILCONTROLLER_H

#include <Arduino.h>

class SailController
{
public:
    SailController();
    void begin(uint8_t in1Pin, uint8_t in2Pin, uint8_t pwmPin, uint8_t stbyPin);

    // Adjusts sail based on apparent wind angle (0-180)
    void setAngleForWind(float apparentWindAngle);

    // Stop the sail motor
    void stop();

    // For logging/display
    float currentDeg = 0.0f;
    int currentDir = 0;

private:
    unsigned long lastUpdateTime = 0;
    uint8_t pinIn1, pinIn2, pinPWM, pinStby;
    // CRITICAL FIX: Define the LEDC Channel for this controller
    const uint8_t ledcChannel = 1;

    // Helper to drive the motor
    void applyPWM(int pwm, int direction);
};

#endif



================================================
FILE: include/SDLogger.h
================================================
#ifndef SDLOGGER_H
#define SDLOGGER_H

#include <Arduino.h>
#include <SPI.h>
#include <SD.h>

class SDLogger {
public:
    SDLogger();
    void begin();

    // Logs the navigation data and motor states to SD card.
 // Change this line in SDLogger.h
void logNavigationData(time_t now, double lat, double lon, double xte, float distance, bool rightOfLine, float rudderDeg, int rudderDir, float sailDeg, int sailDir, uint8_t magAccuracy);

private:
    bool initialized;
};

#endif



================================================
FILE: include/WindSensor.h
================================================
#ifndef WINDSENSOR_H
#define WINDSENSOR_H

#include <Arduino.h>

class WindSensor {
public:
    void begin(uint8_t pinA, uint8_t pinB);
    void update();
    float getApparentWindAngle();
    float getSmoothedWindAngle();
    void calibrate();

private:
    volatile int32_t position = 0;
    int32_t zeroOffset = 0;
    uint8_t pinA, pinB;
    static WindSensor* instance;
    static void handleInterruptA();
    static void handleInterruptB();
    void handleInterrupt(uint8_t pin);

    static const size_t bufferSize = 30;
    float angleBuffer[bufferSize];
    size_t bufferIndex = 0;
    size_t sampleCount = 0;

    void addSample(float angle);
    float getVectorAverage();
};

#endif



================================================
FILE: src/BoatController.cpp
================================================
#include "BoatController.h"
#include "PinConfig.h"
#include <Arduino.h>
#include <time.h>
#include <Wire.h>

void BoatController::begin()
{
    Serial.println("Initializing BoatController...");

    // 1. Persistence
    preferences.begin("boat", false);
    loadState();

    // 2. Filesystem
    if(!LittleFS.begin(true)){
        Serial.println("LittleFS Mount Failed");
    }

    // 3. Hardware Initialization
    gps.begin(Serial1, 9600);
    compass.begin(); // This now calls startCalibration() internally
    wind.begin(WIND_SENSOR_PIN_A, WIND_SENSOR_PIN_B);

    nav.begin();

    rudder.begin(RUDDER_IN1_PIN, RUDDER_IN2_PIN, RUDDER_PWM_PIN, RUDDER_STBY_PIN);
    sail.begin(SAIL_IN1_PIN, SAIL_IN2_PIN, SAIL_PWM_PIN, SAIL_STBY_PIN);

    display.begin();
    sdLogger.begin();

    lastMinute = -1;
    lastMotorMoveMinute = -1;
    lastLogTime = 0;
   
    headingErrorAccumulator = 0;
    errorSampleCount = 0;

    // FreeRTOS Mutex and Tasks Initialization
    navMutex = xSemaphoreCreateMutex();
    if (navMutex != NULL) {
        Serial.println("navMutex created successfully.");

        xTaskCreatePinnedToCore(
            navigatorTask,
            "NavigatorTask",
            4096,
            this,
            1,
            &navigatorTaskHandle,
            0 // PRO_CPU
        );

        xTaskCreatePinnedToCore(
            pilotTask,
            "PilotTask",
            4096,
            this,
            1,
            &pilotTaskHandle,
            1 // APP_CPU
        );
    } else {
        Serial.println("Failed to create navMutex!");
    }
}

void BoatController::loadFieldTestMission() {
    nav.resetWaypoints();
      nav.addWaypoint(52.358297, -2.700906);
       //Waypoint 2 (DMS: 52.21.28, 02.41.57)
      nav.addWaypoint(52.357817, -2.699360);
      //Waypoint 3 (DMS: 52.21.25, 02.42.01)
      nav.addWaypoint(52.356944444, -2.700277778);
      // Waypoint 4 (DMS: 52.21.23, 02.42.01)
      nav.addWaypoint(52.356388889, -2.700277778);
       //Waypoint 5 (Back to Start DMS: 52.21.29, 02.42.02)
      nav.addWaypoint(52.358055556, -2.700555556);

    nav.saveWaypoints();
    Serial.println(">>> MISSION READY: 2 Waypoints Uploaded to Flash.");
    nav.printWaypoints();
}

void BoatController::navigatorTask(void* pvParameters) {
    BoatController* boat = static_cast<BoatController*>(pvParameters);

    while (true) {
        boat->gps.update();
        boat->compass.update();
        boat->wind.update();
        boat->syncTime();

        // 10-Minute State Save (600,000 ms)
        static unsigned long lastSaveMillis = 0;
        if (millis() - lastSaveMillis > 600000) {
            lastSaveMillis = millis();
            boat->saveState();
            Serial.println(">>> 10-MIN UPDATE: State Saved to Preferences.");
        }

        // 6-Hour Voyage Log (21,600,000 ms)
        static unsigned long lastLogMillis = 0;
        if (millis() - lastLogMillis > 21600000) {
            lastLogMillis = millis();
            boat->logData();
            Serial.println(">>> 6-HOUR UPDATE: Data Logged to Voyage.csv.");
        }

        xSemaphoreTake(boat->navMutex, portMAX_DELAY);

        GPSData pos = boat->gps.getData();
        float heading = boat->compass.getHeadingTrue();
        float windAngle = boat->wind.getSmoothedWindAngle();

        boat->nav.update(pos, heading, windAngle);

        if (boat->nav.isAtWaypoint(pos)) {
            Serial.println("********** WAYPOINT REACHED **********");
            boat->nav.advanceWaypoint(pos);
        }

        // Get navigation and motor state for display/logging
        double xte = boat->nav.getXTE();
        float dist = fabs(xte);
        bool rightOfLine = (xte > 0);
        float rDeg = boat->rudder.currentDeg;
        int rDir = boat->rudder.currentDir;
        float sDeg = boat->sail.currentDeg;
        int sDir = boat->sail.currentDir;

        // Rate limit OLED and SD to 1Hz
        static unsigned long lastDisplayLogMillis = 0;
        if (millis() - lastDisplayLogMillis > 1000) {
            lastDisplayLogMillis = millis();

            // 1. Get the current calibration status
            uint8_t calStat = boat->compass.getCalibrationStatus();

            // 2. Log to SD (Time must be fetched)
            time_t now = boat->timeOffset + millis() / 1000;
           
            // Note: Added 'calStat' at the very end of this line
            boat->sdLogger.logNavigationData(now, pos.latitude, pos.longitude, xte, dist, rightOfLine, rDeg, rDir, sDeg, sDir, calStat);

            // 3. Update OLED
            boat->display.update(xte, dist, rightOfLine, rDeg, rDir, sDeg, sDir);
        }

        xSemaphoreGive(boat->navMutex);

        vTaskDelay(pdMS_TO_TICKS(50)); // 20Hz
    }
}

void BoatController::pilotTask(void* pvParameters) {
    BoatController* boat = static_cast<BoatController*>(pvParameters);

    while (true) {
        float headingError = 0;
        bool inIrons = false;
        float apparentWind = boat->wind.getApparentWindAngle();

        if (xSemaphoreTake(boat->navMutex, pdMS_TO_TICKS(500)) == pdTRUE) {
            headingError = boat->nav.getHeadingError();
            inIrons = boat->nav.isInIrons();
            xSemaphoreGive(boat->navMutex);

            if (inIrons) {
                boat->rudder.escapeIrons();
            } else {
                boat->rudder.update(headingError);
            }
            boat->sail.setAngleForWind(apparentWind);
        } else {
            boat->rudder.stop();
            boat->sail.stop();
            Serial.println("Mutex timeout, motors stopped");
        }

        vTaskDelay(pdMS_TO_TICKS(20)); // 50Hz
    }
}

void BoatController::syncTime() {
    TinyGPSPlus& rawGps = gps.getRawGPS();
    if (rawGps.date.isValid() && rawGps.time.isValid()) {
        struct tm tm;
        tm.tm_year = rawGps.date.year() - 1900;
        tm.tm_mon = rawGps.date.month() - 1;
        tm.tm_mday = rawGps.date.day();
        tm.tm_hour = rawGps.time.hour();
        tm.tm_min = rawGps.time.minute();
        tm.tm_sec = rawGps.time.second();
        tm.tm_isdst = 0;
        time_t gpsTime = mktime(&tm);
       
        if (timeOffset == 0 || abs(gpsTime - (timeOffset + (long)(millis() / 1000))) > 2) {
            timeOffset = gpsTime - millis() / 1000;
            Serial.println("Time Synced with GPS");
        }
    }
}

void BoatController::saveState() {
    preferences.putLong("time", timeOffset + millis() / 1000);
    GPSData pos = gps.getData();
    if (pos.valid) {
        preferences.putDouble("lat", pos.latitude);
        preferences.putDouble("lon", pos.longitude);
    }
}

void BoatController::loadState() {
    long savedTime = preferences.getLong("time", 0);
    if (savedTime > 0) {
        timeOffset = savedTime - millis() / 1000;
    }
}

void BoatController::logData() {
    File file = LittleFS.open("/voyage.csv", FILE_APPEND);
    if (!file) return;

    time_t now = timeOffset + millis() / 1000;
    struct tm * ti = localtime(&now);
    GPSData pos = gps.getData();
   
    // Capture Accuracy and Heading
    float currentHeading = compass.getHeadingTrue();
    uint8_t magAccuracy = compass.getCalibrationStatus();

    // CSV format: Date, Time, Lat, Lon, Heading, Accuracy(0-3)
    file.printf("%02d/%02d/%04d,%02d:%02d:%02d,%.9f,%.9f,%.1f,%u\n",
        ti->tm_mday, ti->tm_mon + 1, ti->tm_year + 1900,
        ti->tm_hour, ti->tm_min, ti->tm_sec,
        pos.latitude, pos.longitude,
        currentHeading, magAccuracy
    );
    file.close();

    // Serial monitor feedback for the walk
    Serial.printf("[LOG] T:%02d:%02d:%02d Accu:%u Head:%.1f\n",
                  ti->tm_hour, ti->tm_min, ti->tm_sec, magAccuracy, currentHeading);
}


================================================
FILE: src/CompassManager.cpp
================================================
#include "CompassManager.h"
#include "PinConfig.h"
#include <esp_task_wdt.h>

CompassManager::CompassManager() : quat{0.0f, 0.0f, 0.0f, 1.0f}, hardwareStatus(false) {}

void CompassManager::begin() {
    Serial.println(">>> Initializing BNO085 (SparkFun SPI)...");
    esp_task_wdt_reset();
    pinMode(BNO_INT, INPUT_PULLUP);

    if (myIMU.beginSPI(BNO_CS, BNO_INT, BNO_RST) == false) {
        Serial.println("!!! BNO085 SPI Failure.");
        hardwareStatus = false;
    } else {
        Serial.println(">>> BNO085 SPI Online.");
        hardwareStatus = true;
        myIMU.enableRotationVector(20);
        // Automatically start the calibration engine on boot
        startCalibration();
    }
}

void CompassManager::startCalibration() {
    if (!hardwareStatus) {
        Serial.println("!!! Cannot calibrate. BNO085 not online.");
        return;
    }

    Serial.println(">>> CALIBRATION SEQUENCE STARTING");

    // Enable dynamic calibration for Accel, Gyro, and Mag
    myIMU.setCalibrationConfig(SH2_CAL_ACCEL | SH2_CAL_GYRO | SH2_CAL_MAG);

    Serial.println(">>> Please rotate the boat in figure-8 motions around all 3 axes.");
    Serial.println(">>> Calibration will run for 30 seconds...");

    unsigned long startTime = millis();
    while (millis() - startTime < 30000) {
        // Continuously pump data to prevent SPI stall during calibration
        if (digitalRead(BNO_INT) == LOW) {
            myIMU.wasReset();
            // Pull data to clear internal buffers
            quat.i = myIMU.getQuatI();
            quat.j = myIMU.getQuatJ();
            quat.k = myIMU.getQuatK();
            quat.real = myIMU.getQuatReal();
        }

        if ((millis() - startTime) % 5000 < 10) {
            Serial.print(">>> Calibrating... ");
            Serial.print(30 - ((millis() - startTime) / 1000));
            Serial.println(" seconds remaining.");
            delay(10); // Prevent multi-prints in the same millisecond
        }
        delay(1);
    }

    Serial.println(">>> Calibration window complete. Saving to NVRAM...");
    if (myIMU.saveCalibration()) {
        Serial.println(">>> BNO085 Calibration SAVED successfully.");
    } else {
        Serial.println("!!! BNO085 Calibration SAVE FAILED.");
    }
}

void CompassManager::update() {
    if (!hardwareStatus || digitalRead(BNO_INT) == HIGH) return;
    myIMU.wasReset();
    quat.i = myIMU.getQuatI();
    quat.j = myIMU.getQuatJ();
    quat.k = myIMU.getQuatK();
    quat.real = myIMU.getQuatReal();
}
void CompassManager::saveCalibration() {
    if (hardwareStatus) {
        // 1. Tell the BNO085 to save current offsets to its internal flash (NVRAM)
        if (myIMU.saveCalibration()) {
             Serial.println(">>> BNO085: Offsets saved to sensor memory.");
        } else {
             Serial.println("!!! BNO085: Failed to save calibration.");
        }

        // 2. Effectively "end" calibration by disabling the dynamic calibration bits
        // This sets the calibration config to 0, 0, 0 for Accel, Gyro, and Mag
         // Correct way to disable all calibration
myIMU.setCalibrationConfig(0);
       
        Serial.println(">>> BNO085: Dynamic calibration disabled.");
    }
}


   


uint8_t CompassManager::getCalibrationStatus() {
    if (!hardwareStatus) return 0;

    // This returns a value from 0 to 3:
    // 0 = Unreliable
    // 1 = Low Accuracy
    // 2 = Medium Accuracy
    // 3 = High Accuracy
    return myIMU.getQuatAccuracy();
}

float CompassManager::getHeadingTrue() const {
    if (!hardwareStatus) return 0.0f;
    float y = 2.0f * (quat.real * quat.k + quat.i * quat.j);
    float x = 1.0f - 2.0f * (quat.j * quat.j + quat.k * quat.k);
    float heading = atan2f(y, x) * 180.0f / (float)PI;
    float trueHeading = heading + declination;
    if (trueHeading >= 360.0f) trueHeading -= 360.0f;
    if (trueHeading < 0.0f) trueHeading += 360.0f;
    return trueHeading;
}

// ... (getPitch and getRoll remain the same) ...


================================================
FILE: src/DisplayManager.cpp
================================================
#include "DisplayManager.h"
#include "PinConfig.h"

// Define OLED address, default is 0x3C
#define OLED_ADDR 0x3C

DisplayManager::DisplayManager() : display(SCREEN_WIDTH, SCREEN_HEIGHT, &Wire, -1), initialized(false) {}

void DisplayManager::begin() {
    Wire.begin(OLED_SDA, OLED_SCL);

    // SSD1306_SWITCHCAPVCC = generate display voltage from 3.3V internally
    if (!display.begin(SSD1306_SWITCHCAPVCC, OLED_ADDR)) {
        Serial.println("SSD1306 allocation failed");
        initialized = false;
        return;
    }

    initialized = true;
    display.clearDisplay();
    display.setTextSize(1);
    display.setTextColor(SSD1306_WHITE);
    display.setCursor(0, 0);
    display.println("AutoBoat Booting...");
    display.display();
}

void DisplayManager::update(double xte, float distance, bool rightOfLine, float rudderDeg, int rudderDir, float sailDeg, int sailDir) {
    if (!initialized) return;

    display.clearDisplay();
    display.setCursor(0, 0);

    // Navigation Data
    display.println("--- Navigation ---");
    display.print("XTE: ");
    display.print(xte, 2);
    display.println(" m");

    display.print("Dist: ");
    display.print(distance, 1);
    display.println(" m");

    display.print("Side: ");
    display.println(rightOfLine ? "Right of Line" : "Left of Line");

    // Motor Data
    display.println("--- Motors ---");
    display.print("Rudder: ");
    display.print(rudderDeg, 1);
    display.print(" deg ");
    display.println((rudderDir > 0) ? "(R)" : ((rudderDir < 0) ? "(L)" : "(-)"));

    display.print("Sail:   ");
    display.print(sailDeg, 1);
    display.print(" deg ");
    display.println((sailDir > 0) ? "(Out)" : ((sailDir < 0) ? "(In)" : "(-)"));

    display.display();
}



================================================
FILE: src/GPSManager.cpp
================================================
#include "GPSManager.h"

void GPSManager::begin(HardwareSerial &serialPort, uint32_t baud) {
    gpsSerial = &serialPort;
    // Serial port initialization is handled in main.cpp
}

void GPSManager::update() {
    while (gpsSerial->available() > 0) {
        // READ FROM HARDWARE: Read byte from GPS serial stream
        gps.encode(gpsSerial->read());
    }
}

GPSData GPSManager::getData() {
    GPSData data;
    data.latitude = gps.location.lat();
    data.longitude = gps.location.lng();
    data.speedKmph = gps.speed.kmph();
    data.courseDeg = gps.course.deg();
    data.valid = gps.location.isValid() && gps.location.age() < 2000;
    return data;
}

double GPSManager::courseTo(double targetLat, double targetLon) {
    return TinyGPSPlus::courseTo(gps.location.lat(), gps.location.lng(), targetLat, targetLon);
}

double GPSManager::distanceTo(double targetLat, double targetLon) {
    return TinyGPSPlus::distanceBetween(gps.location.lat(), gps.location.lng(), targetLat, targetLon);
}


================================================
FILE: src/main.cpp
================================================
#include <Arduino.h>
#include <Wire.h>
#include "BoatController.h"
#include "PinConfig.h"

BoatController boat;

void setup() {
    Serial.begin(115200);
    delay(1000);
    Serial.println("Autonomous Boat System Booting...");

    // 1. Calibrate the Compass (BNO085)
    // This will pause the program for 30 seconds while you spin the boat in all orientations.
    // Remove or comment this out after you have calibrated once
    boat.compass.startCalibration();
   
    // 2. Initialize GPS Serial Port
    Serial1.begin(9600, SERIAL_8N1, GPS_RX_PIN, GPS_TX_PIN);

    // 3. Initialize Boat Hardware and Persistence
    boat.begin();

    // 4. Load the Specific Field Test Waypoints
    boat.loadFieldTestMission();


    Serial.println("Setup Complete. Standing by for GPS Lock...");
}

void loop() {
    // Main loop is not needed as FreeRTOS tasks handle navigation and pilot
    vTaskDelete(NULL);
}


================================================
FILE: src/NavigationManager.cpp
================================================
#include <Arduino.h>
#include "NavigationManager.h"
#include <TinyGPSPlus.h>
#include <Preferences.h>
#include <math.h>

Preferences prefs;

NavigationManager::NavigationManager() :
    waypointCount(0),
    currentIndex(0),
    headingError(0),
    inIrons(false),
    lastWindRelative(180),
    xte(0.0),
    firstLock(true) {
    memset(latitudes, 0, sizeof(latitudes));
    memset(longitudes, 0, sizeof(longitudes));
}

void NavigationManager::begin() {
    prefs.begin("waypoints", false);
    loadWaypoints();
}

void NavigationManager::update(const GPSData& pos, float heading, float windAngle) {
    if (waypointCount == 0 || !pos.valid) return;

    // Initialize the very first start point if this is our first valid GPS lock
    if (firstLock) {
        startLat = pos.latitude;
        startLon = pos.longitude;
        firstLock = false;
    }

    double targetLat = latitudes[currentIndex];
    double targetLon = longitudes[currentIndex];

    // 1. Calculate the 'Rail' Error (XTE)
    xte = calculateXTE(startLat, startLon, targetLat, targetLon, pos.latitude, pos.longitude);

    // 2. Point-and-Shoot: Direct bearing to the target
    double directCourse = TinyGPSPlus::courseTo(pos.latitude, pos.longitude, targetLat, targetLon);

    // 3. Correction: For every 1 meter of error, aim 2 degrees back to center
    // Limit to 30 degrees to avoid sailing directly sideways
    float correction = (float)xte * 2.0f;
    correction = constrain(correction, -30.0f, 30.0f);

    float correctedCourse = (float)directCourse - correction;

    // 4. Normalize heading error (-180 to 180)
    headingError = correctedCourse - heading;
    headingError = fmodf(headingError + 540.0f, 360.0f) - 180.0f;

    // 5. Wind / Irons Logic
    float targetCourse = (float)directCourse;
    float windRelative = fmodf(windAngle - targetCourse + 540.0f, 360.0f) - 180.0f;
    windRelative = fabsf(windRelative);

    if (windRelative < 25 || (lastWindRelative < 35 && windRelative < 35)) {
        inIrons = true;
    } else {
        inIrons = false;
    }
    lastWindRelative = windRelative;
}

// Math helper for XTE
double NavigationManager::calculateXTE(double sLat, double sLon, double tLat, double tLon, double pLat, double pLon) {
    double R = 6371000.0; // Earth radius in meters
    double d13 = TinyGPSPlus::distanceBetween(sLat, sLon, pLat, pLon) / R;
    double brng13 = toRadians(TinyGPSPlus::courseTo(sLat, sLon, pLat, pLon));
    double brng12 = toRadians(TinyGPSPlus::courseTo(sLat, sLon, tLat, tLon));

    return asin(sin(d13) * sin(brng13 - brng12)) * R;
}

double NavigationManager::toRadians(double degree) {
    return degree * M_PI / 180.0;
}

float NavigationManager::getHeadingError() const { return headingError; }
double NavigationManager::getXTE() const { return xte; }
bool NavigationManager::isInIrons() const { return inIrons; }

bool NavigationManager::isAtWaypoint(const GPSData& pos) const {
    if (waypointCount == 0 || !pos.valid) return false;
    return distanceTo(pos.latitude, pos.longitude, latitudes[currentIndex], longitudes[currentIndex]) < WAYPOINT_RADIUS_METERS;
}

bool NavigationManager::hasNextWaypoint() const {
    return (currentIndex + 1 < waypointCount);
}

void NavigationManager::advanceWaypoint(const GPSData& pos) {
    if (hasNextWaypoint()) {
        // The waypoint we just finished becomes the NEW start point for the next leg
        startLat = latitudes[currentIndex];
        startLon = longitudes[currentIndex];
       
        currentIndex++;
        prefs.putInt("currentIndex", currentIndex);
        Serial.print("Advancing. New Target WP: "); Serial.println(currentIndex + 1);
    }
}

// Storage functions updated for Double precision
void NavigationManager::saveWaypoints() {
    prefs.putInt("count", waypointCount);
    prefs.putInt("currentIndex", currentIndex);
    char key[10];
    for (int i = 0; i < waypointCount; i++) {
        snprintf(key, sizeof(key), "lat%d", i);
        prefs.putDouble(key, latitudes[i]);
        snprintf(key, sizeof(key), "lon%d", i);
        prefs.putDouble(key, longitudes[i]);
    }
}

void NavigationManager::loadWaypoints() {
    waypointCount = min(prefs.getInt("count", 0), maxWaypoints);
    currentIndex = prefs.getInt("currentIndex", 0);
    if (currentIndex >= waypointCount) currentIndex = 0;

    char key[10];
    for (int i = 0; i < waypointCount; i++) {
        snprintf(key, sizeof(key), "lat%d", i);
        latitudes[i] = prefs.getDouble(key, 0.0);
        snprintf(key, sizeof(key), "lon%d", i);
        longitudes[i] = prefs.getDouble(key, 0.0);
    }
}

void NavigationManager::resetWaypoints() {
    waypointCount = 0;
    currentIndex = 0;
    prefs.putInt("count", 0);
    prefs.putInt("currentIndex", 0);
}

void NavigationManager::addWaypoint(double lat, double lon) {
    if (waypointCount >= maxWaypoints) return;
    latitudes[waypointCount] = lat;
    longitudes[waypointCount] = lon;
    waypointCount++;
}

double NavigationManager::distanceTo(double lat1, double lon1, double lat2, double lon2) const {
    return TinyGPSPlus::distanceBetween(lat1, lon1, lat2, lon2);
}

void NavigationManager::printWaypoints() const {
    Serial.println(">>> WAYPOINT LIST (TASKS) <<<");
    if (waypointCount == 0) {
        Serial.println("No waypoints loaded.");
        return;
    }
    for (int i = 0; i < waypointCount; i++) {
        Serial.print("WP ");
        Serial.print(i + 1);
        if (i == currentIndex) Serial.print(" [ACTIVE]");
        Serial.print(": ");
        Serial.print(latitudes[i], 9);
        Serial.print(", ");
        Serial.println(longitudes[i], 9);
    }
    Serial.println("-----------------------------");
}



================================================
FILE: src/RudderController.cpp
================================================
#include <Arduino.h>
#include "RudderController.h"
#include "PinConfig.h"
#include <driver/ledc.h>

void RudderController::begin(uint8_t in1, uint8_t in2, uint8_t pwm, uint8_t stby)
{
    pinIn1 = in1;
    pinIn2 = in2;
    pinPWM = pwm;
    pinStby = stby;

    pinMode(pinIn1, OUTPUT);
    pinMode(pinIn2, OUTPUT);
    pinMode(pinStby, OUTPUT);
    digitalWrite(pinStby, HIGH); // Enable the motor driver

    // Setup ESP32 PWM (Channel 0, 20kHz, 8-bit resolution)
    ledcSetup(ledcChannel, PWM_FREQ_MOTOR, PWM_RESOLUTION);
    ledcAttachPin(pinPWM, ledcChannel);

    lastUpdateTime = millis();
    stop();
}

void RudderController::update(float headingError)
{
    // Simple Proportional Control
    // headingError is the difference between target and current heading (-180 to 180).

    // Calculate PWM output
    // We want a higher PWM for larger errors.
    int pwm = (int)(headingError * Kp);

    // Determine direction
    // If error is positive, we need to turn towards positive (Right/Starboard)
    // Assuming Motor Direction 1 is Starboard, -1 is Port.

    int direction = 0;
    if (pwm > 10) { // Deadband
        direction = 1;
    } else if (pwm < -10) {
        direction = -1;
    }

    // Apply PWM
    applyPWM(abs(pwm), direction);
}

void RudderController::stop() {
    applyPWM(0, 0);
}

void RudderController::applyPWM(int pwm, int direction)
{
    // Constrain PWM to 0-255 range
    int safePWM = constrain(abs(pwm), 0, 255);

    // Calculate simulated degrees moved based on open loop PWM duration
    unsigned long now = millis();
    float dt = (now - lastUpdateTime) / 1000.0f;
    lastUpdateTime = now;

    // Estimate degrees based on PWM speed. Max 255 PWM = ~15 deg/sec.
    float degPerSec = (safePWM / 255.0f) * 15.0f;
    currentDeg += (direction * degPerSec * dt);

    // Boundary constraint (-45 to 45 deg)
    currentDeg = constrain(currentDeg, -45.0f, 45.0f);
    currentDir = direction;

    if (direction > 0)
    {
        digitalWrite(pinIn1, HIGH);
        digitalWrite(pinIn2, LOW);
    }
    else if (direction < 0)
    {
        digitalWrite(pinIn1, LOW);
        digitalWrite(pinIn2, HIGH);
    }
    else
    {
        digitalWrite(pinIn1, LOW);
        digitalWrite(pinIn2, LOW);
        safePWM = 0;
    }

    ledcWrite(ledcChannel, safePWM);
}

void RudderController::escapeIrons()
{
    // Moves the rudder to one side to get the boat moving
    applyPWM(200, 1);
    delay(1500);    // Note: This blocks other sensors for 1.5s
    applyPWM(0, 0); // Stop
}



================================================
FILE: src/SailController.cpp
================================================
#include "SailController.h"
#include "PinConfig.h"
#include <driver/ledc.h>

SailController::SailController() {}

void SailController::begin(uint8_t in1, uint8_t in2, uint8_t pwm, uint8_t stby)
{
    pinIn1 = in1;
    pinIn2 = in2;
    pinPWM = pwm;
    pinStby = stby;

    pinMode(pinIn1, OUTPUT);
    pinMode(pinIn2, OUTPUT);
    pinMode(pinStby, OUTPUT);

    // Enable the driver
    digitalWrite(pinStby, HIGH);

    // Setup ESP32 PWM
    ledcSetup(ledcChannel, PWM_FREQ_MOTOR, PWM_RESOLUTION);
    ledcAttachPin(pinPWM, ledcChannel);

    lastUpdateTime = millis();
    stop();
}

void SailController::stop() {
    applyPWM(0, 0);
}

void SailController::setAngleForWind(float apparentWindAngle) {
    // Map apparent wind (0-180) to sail position (0-100%)
    // 0 deg (Head to wind) -> Sail in (Tight, 0%)
    // 180 deg (Downwind) -> Sail out (Loose, 100%)

    // Note: Since this hardware configuration (H-Bridge) lacks a position feedback sensor
    // defined in PinConfig.h, we cannot implement a closed-loop position control here.
    // We would need a potentiometer or encoder on the winch.

    // Placeholder logic:
    // float targetPosition = (abs(apparentWindAngle) / 180.0f) * 100.0f;
    // Serial.printf("Sail Target: %.1f%%\n", targetPosition);
}

void SailController::applyPWM(int pwm, int direction)
{
    // Constrain PWM
    int safePWM = constrain(abs(pwm), 0, 255);

    // Calculate simulated degrees moved based on open loop PWM duration
    unsigned long now = millis();
    float dt = (now - lastUpdateTime) / 1000.0f;
    lastUpdateTime = now;

    // Estimate degrees based on PWM speed. Max 255 PWM = ~20 deg/sec.
    float degPerSec = (safePWM / 255.0f) * 20.0f;
    currentDeg += (direction * degPerSec * dt);

    // Boundary constraint (0 to 90 deg)
    currentDeg = constrain(currentDeg, 0.0f, 90.0f);
    currentDir = direction;

    if (direction > 0)
    {
        digitalWrite(pinIn1, HIGH);
        digitalWrite(pinIn2, LOW);
    }
    else if (direction < 0)
    {
        digitalWrite(pinIn1, LOW);
        digitalWrite(pinIn2, HIGH);
    }
    else
    {
        digitalWrite(pinIn1, LOW);
        digitalWrite(pinIn2, LOW);
        safePWM = 0;
    }

    ledcWrite(ledcChannel, safePWM);
}



================================================
FILE: src/SDLogger.cpp
================================================
#include "SDLogger.h"
#include "PinConfig.h"

SDLogger::SDLogger() : initialized(false) {}

void SDLogger::begin() {
    Serial.println("Initializing SD card...");

    // The BNO085 uses SPI. We will share the bus.
    // Ensure BNO CS is not active while initializing SD.
    pinMode(BNO_CS, OUTPUT);
    digitalWrite(BNO_CS, HIGH);

    if (!SD.begin(SD_CS_PIN)) {
        Serial.println("SD Card initialization failed!");
        initialized = false;
        return;
    }

    initialized = true;
    Serial.println("SD Card initialized.");

    // Create header if file doesn't exist
    if (!SD.exists("/nav_log.csv")) {
        File file = SD.open("/nav_log.csv", FILE_WRITE);
        if (file) {
            // Update the header string (Line ~25)
            file.println("time,latitude,longitude,XTE,distance,side_of_line,rudder_deg,rudder_dir,sail_deg,sail_dir,mag_acc");
            file.close();
        }
    }
}

// 1. ADD 'uint8_t magAccuracy' TO THE END OF THIS LINE
void SDLogger::logNavigationData(time_t now, double lat, double lon, double xte, float distance, bool rightOfLine, float rudderDeg, int rudderDir, float sailDeg, int sailDir, uint8_t magAccuracy) {
    if (!initialized) return;

    // Convert time to string
    struct tm * ti = localtime(&now);
    char timeStr[24];
    sprintf(timeStr, "%04d-%02d-%02d %02d:%02d:%02d",
        ti->tm_year + 1900, ti->tm_mon + 1, ti->tm_mday,
        ti->tm_hour, ti->tm_min, ti->tm_sec
    );

    File file = SD.open("/nav_log.csv", FILE_APPEND);
    if (!file) {
        Serial.println("Failed to open nav_log.csv for appending");
        return;
    }

    // 2. REPLACE THE OLD PRINTF WITH THIS UPDATED VERSION
    // Note the ,%u at the end of the quotes and magAccuracy at the end of the list
    file.printf("%s,%.9f,%.9f,%.2f,%.2f,%s,%.2f,%d,%.2f,%d,%u\n",
        timeStr,
        lat, lon,
        xte, distance,
        rightOfLine ? "Right" : "Left",
        rudderDeg, rudderDir,
        sailDeg, sailDir,
        magAccuracy // <--- This matches the %u above
    );

    file.close();
}




================================================
FILE: src/WindSensor.cpp
================================================
#include "WindSensor.h"
#include <math.h>
#include <ESP32Encoder.h>

// The encoder object MUST be declared globally or statically
static ESP32Encoder encoder;

// Define the steps per revolution for your encoder
// The RM22SC0010B10F1C00 is 1024 steps per revolution
#define STEPS_PER_REV 1024.0

WindSensor *WindSensor::instance = nullptr;

void WindSensor::begin(uint8_t a, uint8_t b)
{
    // We no longer need to use pinMode(INPUT) as the library handles it

    // Set up the encoder. It handles all the interrupt logic internally.
    encoder.attachFullQuad(a, b);

    // Set initial position and enable counting
    encoder.setCount(0);
    encoder.clearCount();

    // Set the max count to prevent overflow, though int32_t is large
    encoder.setFilter(1023); // Set debounce filter for cleaner readings

    // Initialize buffer
    bufferIndex = 0;
    sampleCount = 0;
    for(int i=0; i<bufferSize; i++) angleBuffer[i] = 0.0;
}

void WindSensor::calibrate()
{
    // The zero offset is now set by resetting the library's internal counter
    encoder.clearCount();
    zeroOffset = 0;
}

float WindSensor::getApparentWindAngle()
{
    // 1. Get the raw count from the library
    // READ FROM HARDWARE: Get raw encoder count
    int32_t raw_steps = encoder.getCount();

    // 2. Calculate the position within a single 360-degree rotation
    // Use the fmodf() function for accurate floating-point modulus
    float steps_f = fmodf((float)raw_steps, STEPS_PER_REV);

    // 3. Ensure the result is positive (0 to 1024)
    if (steps_f < 0)
    {
        steps_f += STEPS_PER_REV;
    }

    // 4. Convert steps to angle (0.0 to 360.0)
    float angle = (steps_f / STEPS_PER_REV) * 360.0;

    addSample(angle);
    return angle;
}

void WindSensor::update()
{
    // The library handles updates in the background, so this function remains empty,
    // but the main loop calls it for consistency.
    // However, calling getApparentWindAngle updates the buffer.
    getApparentWindAngle();
}

void WindSensor::addSample(float angle) {
    angleBuffer[bufferIndex] = angle;
    bufferIndex = (bufferIndex + 1) % bufferSize;
    if (sampleCount < bufferSize) sampleCount++;
}

float WindSensor::getSmoothedWindAngle() {
    return getVectorAverage();
}

float WindSensor::getVectorAverage() {
    if (sampleCount == 0) return 0.0;

    float sumSin = 0;
    float sumCos = 0;

    for (size_t i = 0; i < sampleCount; i++) {
        float rad = angleBuffer[i] * (float)PI / 180.0f;
        sumSin += sinf(rad);
        sumCos += cosf(rad);
    }

    float avgRad = atan2f(sumSin, sumCos);
    float avgDeg = avgRad * 180.0 / PI;

    if (avgDeg < 0) avgDeg += 360.0;
    return avgDeg;

Slava Asipenko

unread,
May 2, 2026, 3:14:34 PM (3 days ago) May 2
to Damon McMillan, Alastair Lidstone, James, Microtransat
Hello everyone,

> Are there any other builds that are close to completion? Keen to hear from anyone who is working on something!

I can answer this! Just posted a Brave Puffin update: https://bravepuffin.com/posts/2026-may-update/

TL;DR it's very much alive - been busy all of 2025 and 2026 building and testing. With some luck it should launch this summer.

Slava



Alec Adams

unread,
May 4, 2026, 2:35:15 AM (yesterday) May 4
to Slava Asipenko, Damon McMillan, Alastair Lidstone, James, Microtransat
Messed it up again, here's the zip file A&S

On Mon, May 4, 2026 at 6:59 AM Alec Adams <alec...@gmail.com> wrote:
We have started a new code based on the code Colin sent, abersailbot minimum viable control (thank you Colin).  A couple of people said that ours was too complicated and didn't need the cores splitting  so back to work and this is what turned out.  This is what we have so far, still a bit rough though https://github.com/Alfromuk/AMVCS_Core.git and another flowery description from notebooklm  https://notebooklm.google.com/notebook/a470d68d-b06f-45b3-9016-66cab1054293 but you need to log into Google to get it to work. Tomorrow we hope to have the soldering done on our test rig and then we can test both programs. Also a few photos of the boat. At the moment it is being held together with fresh air and clothes pegs. 

AMVCS_Core-main.zip

Philip Smith

unread,
May 4, 2026, 1:48:58 PM (15 hours ago) May 4
to Microtransat
I'm hoping to launch my boat again this year, ideally this month or next (more likely next I fear). I've been testing out at a local pond and getting generally confused about why it isn't sailing too well. Think I've concluded that trying to head into a strong wind is incompatible with sailing across the same wind as there is too much heel across wind (sail is sheeted in too much). So I've added more keel weight and reduced sheeting to compromise between upwind and across-wind, and given up coping with strong winds, ie above F4. Also been trying to reduce rudder battery use, but have not made much progress on that front, so adding another solar panel. Software then needs thorough testing to remove any new bugs I've introduced. Plus I've been told my website doesn't work, so that needs some work too.

This is the same boat that was rescued a couple of years ago off the Isle of Wight. But a bigger rudder and more keel, and extra PV, plus some mods to boom spring system. I've not decided on launch point as Weymouth is convenient if starting from home (Cambridge), but am on holiday in Cornwall, near Lands End, at the end of the month which also provides some good locations.

Phil

Reply all
Reply to author
Forward
0 new messages