Skip to content

feat(apds9960): Add OLED color lamp example.#372

Open
Kaanoz-en wants to merge 1 commit intomainfrom
feat/apds9960-color-lamp
Open

feat(apds9960): Add OLED color lamp example.#372
Kaanoz-en wants to merge 1 commit intomainfrom
feat/apds9960-color-lamp

Conversation

@Kaanoz-en
Copy link
Copy Markdown
Contributor

@Kaanoz-en Kaanoz-en commented Apr 10, 2026

Summary

closes #331

Adds an intermediate example (color_lamp.py) demonstrating a reactive color lamp using the APDS9960 RGBC sensor and the SSD1327 OLED display. The screen dynamically changes its background brightness based on the room's ambient light and detects the dominant color of objects placed near it.

Changes

  • Added lib/apds9960/examples/color_lamp.py.
  • Implemented an auto-calibration phase on startup to sample ambient light and dynamically set the MAX_CLEAR baseline.
  • Designed a round-screen optimized UI, placing the raw RGBC values in a centered 2x2 grid to avoid screen edge clipping.
  • Added a dynamic background greyscale feature that maps ambient light intensity to the OLED's 0-15 greyscale range.
  • Implemented adaptive text contrast (text switches between black and white depending on the background brightness) to ensure readability.
  • Ensured proper hardware cleanup (display.power_off()) in the finally block.

Checklist

  • ruff check passes
  • python -m pytest tests/ -k mock -v passes (no mock test broken)
  • Tested on hardware (if applicable)
  • README updated (if adding/changing public API)
  • Examples added/updated (if applicable)
  • Commit messages follow <scope>: <Description.> format

Copilot AI review requested due to automatic review settings April 10, 2026 10:37
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a new intermediate APDS9960 example that combines the RGBC sensor with the SSD1327 OLED to create a “reactive color lamp” UI that adapts background brightness to ambient light and shows a dominant-color label plus raw RGBC readings.

Changes:

  • Added color_lamp.py example that reads APDS9960 RGBC values and renders a round-screen-friendly UI on SSD1327.
  • Implemented a startup ambient-light calibration step to derive a dynamic clear-channel baseline for background brightness mapping.
  • Added display shutdown behavior on exit via display.power_off() in a finally block.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +67 to +69
# Add a small buffer to the max calibration so it doesn't max out too easily
MAX_CLEAR = max_clear_calib + (max_clear_calib // CALIB_BUFFER_DIVISOR)
print("Calibration complete. MAX_CLEAR set to:", MAX_CLEAR)
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

MAX_CLEAR is computed at runtime during calibration but is named like a module constant. Consider renaming to something like max_clear/max_clear_limit (and keeping true constants uppercase) to avoid implying it’s compile-time/static.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch regarding the PEP 8 naming conventions. You're absolutely right, since it's computed at runtime it shouldn't be formatted as a static constant. I've renamed it to max_clear_limit.

Comment on lines +114 to +121
except KeyboardInterrupt:
print("\nColor lamp stopped.")
finally:
# Clean up and power off display on exit
display.fill(0)
display.show()
sleep_ms(100)
display.power_off()
Copy link

Copilot AI Apr 10, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The example enables the APDS9960 light sensor but never disables it on exit. Since you’re already doing display cleanup in finally, consider calling apds.disable_light_sensor() (and/or apds.power_off()) there as well to avoid leaving the sensor powered and drawing current after Ctrl+C.

Copilot uses AI. Check for mistakes.
Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent point on hardware cleanup. I've added apds.disable_light_sensor() to the finally block to ensure the sensor doesn't keep drawing current after the script exits.

@Kaanoz-en Kaanoz-en requested a review from nedseb April 10, 2026 10:42
@Kaanoz-en Kaanoz-en force-pushed the feat/apds9960-color-lamp branch from 83892eb to 0c59829 Compare April 10, 2026 10:46
@nedseb nedseb requested review from Charly-sketch and Copilot and removed request for nedseb April 10, 2026 12:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 1 out of 1 changed files in this pull request and generated no new comments.


💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

@nedseb
Copy link
Copy Markdown
Contributor

nedseb commented Apr 10, 2026

💡 Suggestion: cette PR utilise le SSD1327 directement. Le projet a maintenant steami_screen qui gère la conversion de couleur et le positionnement automatique.

Tu pourrais utiliser :

from steami_screen import Screen, SSD1327Display
display = SSD1327Display(ssd1327.WS_OLED_128X128_SPI(spi, dc, res, cs))
screen = Screen(display)

screen.clear()
screen.title("Color")
screen.value(color_name)
screen.bar(brightness, max_val=255)
screen.show()

Le wrapper SSD1327Display gère la conversion RGB→greyscale automatiquement. Pas obligatoire, mais ça simplifierait le code d'affichage.

@Charly-sketch
Copy link
Copy Markdown
Contributor

Review

Très bon travail sur cet exemple.
L’idée est excellente et l’exécution est propre : calibration automatique, UI adaptée à un écran rond, contraste dynamique. C’est un exemple riche et pédagogique qui montre bien comment combiner le capteur APDS9960 avec un affichage OLED.

L’ensemble est globalement propre, mais il y a quelques points à améliorer pour le rendre plus robuste et mieux aligné avec le projet.


1. Division potentiellement dangereuse

bg_color = (clamped_c * 15) // max_clear_limit

Si max_clear_limit == 0, cela provoque une division par zéro.

Même si peu probable avec le baseline actuel, il vaut mieux sécuriser.

Suggestion :

if max_clear_limit <= 0:
    max_clear_limit = 1

bg_color = (clamped_c * 15) // max_clear_limit

2. Clamp incohérent

clamped_c = max(0, min(c, max_clear_limit))

Tu clamps avec 0, alors que tu utilises MIN_CLEAR ailleurs pour la logique de détection.

Cela crée une incohérence entre :

  • l’affichage (basé sur 0)
  • la logique métier (basée sur MIN_CLEAR)

À harmoniser pour avoir un comportement cohérent.


3. Détection de couleur trop sensible

if r > g and r > b:

La détection est très sensible au bruit, ce qui peut provoquer du flickering entre couleurs.

Suggestion : ajouter un seuil de dominance :

THRESHOLD = 20

if r > g + THRESHOLD and r > b + THRESHOLD:

4. Pas de normalisation RGB

Les valeurs RGB sont utilisées directement :

r = apds.red_light()
g = apds.green_light()
b = apds.blue_light()

Le problème est qu’elles dépendent fortement de la lumière ambiante (c), ce qui peut fausser la détection des couleurs.

Suggestion : normaliser avec c :

if c > 0:
    r_n = r / c
    g_n = g / c
    b_n = b / c

Puis utiliser ces valeurs pour la détection.


5. Calibration perfectible (non bloquant)

max_clear_calib = max(max_clear_calib, c)

Tu prends uniquement le maximum, ce qui est sensible à un pic ponctuel.

Ce n’est pas bloquant, mais une moyenne serait plus robuste.


6. Utilisation de steami_screen (suggestion)

Tu utilises directement ssd1327.

Depuis, le projet a introduit steami_screen qui simplifie ce type d’affichage (widgets, gestion automatique, etc.).

Ce n’était pas encore disponible au moment de la PR, mais tu peux maintenant t’en servir pour simplifier le code.


7. README non mis à jour

L’exemple est ajouté mais n’est pas référencé dans le README du driver.

À faire :

  • ajouter color_lamp.py dans la table des exemples

8. Description de la PR

Il manque la référence à l’issue dans la description.

À ajouter :

Closes #XXX

Conclusion

Très bon exemple, visuel et interactif, avec une vraie valeur pédagogique.
La gestion du contraste, la calibration et l’UI sont particulièrement bien pensées.

À corriger principalement :

  • sécuriser la division
  • améliorer la détection de couleur (seuil)
  • normaliser les RGB
  • améliorer la calibration (optionnel)
  • mettre à jour le README
  • ajouter le Closes #issue
  • éventuellement utiliser steami_screen

Avec ces ajustements, ce sera un excellent exemple pour le repository.

@Kaanoz-en Kaanoz-en force-pushed the feat/apds9960-color-lamp branch from 0c59829 to db8a050 Compare April 13, 2026 09:32
@Kaanoz-en
Copy link
Copy Markdown
Contributor Author

Refactored color_lamp.py based on review feedback and hardware testing.

Improvements :
Upgraded the calibration phase to use an average (total_c // CALIB_SAMPLES) instead of a single peak value for better noise resilience. Added a zero-division safeguard for max_clear_limit.
Updated the ambient light clamping floor to use MIN_CLEAR to ensure consistency between the display output and the core business logic. Also added the exemple to the README.md.

Decisions :
Hardware testing revealed that mathematically normalizing RGB values against the Clear channel (r/c) actually degrades accuracy on the APDS9960. The Clear photodiode has a different spectral absorption curve, which skews ratios unpredictably under varying light sources. Instead, a flat DOMINANCE_THRESHOLD (3) was applied to the raw values. This successfully eliminates flickering and safely accounts for the silicon's naturally lower sensitivity to blue light. I didn't use 20 as recommended for the threshold because the differences between the RGB values were often small and such a threshold created higher variability in results and more flickering.
Opted to retain the raw ssd1327 framebuf implementation : while steami_screen is great for standardization, its enforced static layout would require removing the dynamic contrast background and the custom 2x2 data grid—which are the primary pedagogical and visual features of this specific example.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat(apds9960): Add color lamp example with OLED display.

4 participants