Skip to content

Commit a6b010b

Browse files
committed
Support Hardkernel ODROID-Go
Support for Hardkernel ESP32 device: ODROID-Go (The old one from 2018) * https://github.com/hardkernel/ODROID-GO/ * https://wiki.odroid.com/odroid_go/odroid_go What worked: * Display * Buttons * Crossbar * Wifi * Battery * blue LED TODO: * Speaker The blue LED is "coupled" with the button/crossbar press.
1 parent 06d64b7 commit a6b010b

5 files changed

Lines changed: 361 additions & 21 deletions

File tree

Lines changed: 253 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,253 @@
1+
print("odroid_go.py initialization")
2+
3+
# Hardware initialization for Hardkernel ODROID-Go
4+
# https://github.com/hardkernel/ODROID-GO/
5+
# https://wiki.odroid.com/odroid_go/odroid_go
6+
7+
import time
8+
9+
import ili9341
10+
import lcd_bus
11+
import lvgl as lv
12+
import machine
13+
import mpos.ui
14+
from machine import ADC, Pin
15+
from micropython import const
16+
from mpos import InputManager
17+
18+
# Display settings:
19+
SPI_HOST = const(1)
20+
SPI_FREQ = const(40000000)
21+
22+
LCD_SCLK = const(18)
23+
LCD_MOSI = const(23)
24+
LCD_DC = const(21)
25+
LCD_CS = const(5)
26+
LCD_BL = const(32)
27+
LCD_RST = const(33)
28+
LCD_TYPE = const(2) # ILI9341 type 2
29+
30+
TFT_VER_RES = const(320)
31+
TFT_HOR_RES = const(240)
32+
33+
34+
# Button settings:
35+
BUTTON_MENU = const(13)
36+
BUTTON_VOLUME = const(0)
37+
BUTTON_SELECT = const(27)
38+
BUTTON_START = const(39)
39+
40+
BUTTON_B = const(33)
41+
BUTTON_A = const(32)
42+
43+
# The crossbar pin numbers:
44+
CROSSBAR_X = const(34)
45+
CROSSBAR_Y = const(35)
46+
47+
48+
# Misc settings:
49+
LED_BLUE = const(2)
50+
BATTERY_PIN = const(36)
51+
BATTERY_RESISTANCE_NUM = const(2)
52+
SPEAKER_ENABLE_PIN = const(25)
53+
SPEAKER_PIN = const(26)
54+
55+
56+
print("odroid_go.py turn on blue LED")
57+
blue_led = machine.Pin(LED_BLUE, machine.Pin.OUT)
58+
blue_led.on()
59+
60+
61+
print("odroid_go.py machine.SPI.Bus() initialization")
62+
try:
63+
spi_bus = machine.SPI.Bus(host=SPI_HOST, mosi=LCD_MOSI, sck=LCD_SCLK)
64+
except Exception as e:
65+
print(f"Error initializing SPI bus: {e}")
66+
print("Attempting hard reset in 3sec...")
67+
time.sleep(3)
68+
machine.reset()
69+
70+
print("odroid_go.py lcd_bus.SPIBus() initialization")
71+
display_bus = lcd_bus.SPIBus(spi_bus=spi_bus, freq=SPI_FREQ, dc=LCD_DC, cs=LCD_CS)
72+
73+
print("odroid_go.py ili9341.ILI9341() initialization")
74+
try:
75+
mpos.ui.main_display = ili9341.ILI9341(
76+
data_bus=display_bus,
77+
display_width=TFT_HOR_RES,
78+
display_height=TFT_VER_RES,
79+
color_space=lv.COLOR_FORMAT.RGB565,
80+
color_byte_order=ili9341.BYTE_ORDER_BGR,
81+
rgb565_byte_swap=True,
82+
reset_pin=LCD_RST,
83+
reset_state=ili9341.STATE_LOW,
84+
backlight_pin=LCD_BL,
85+
backlight_on_state=ili9341.STATE_PWM,
86+
)
87+
except Exception as e:
88+
print(f"Error initializing ILI9341: {e}")
89+
print("Attempting hard reset in 3sec...")
90+
time.sleep(3)
91+
machine.reset()
92+
93+
print("odroid_go.py display.init()")
94+
mpos.ui.main_display.init(type=LCD_TYPE)
95+
mpos.ui.main_display.set_rotation(lv.DISPLAY_ROTATION._270)
96+
mpos.ui.main_display.set_power(True)
97+
mpos.ui.main_display.set_color_inversion(False)
98+
mpos.ui.main_display.set_backlight(25)
99+
100+
print("odroid_go.py lv.init() initialization")
101+
lv.init()
102+
103+
104+
print("odroid_go.py Battery initialization...")
105+
from mpos import BatteryManager
106+
107+
108+
def adc_to_voltage(adc_value):
109+
return adc_value * BATTERY_RESISTANCE_NUM
110+
111+
112+
BatteryManager.init_adc(BATTERY_PIN, adc_to_voltage)
113+
114+
115+
print("odroid_go.py button initialization...")
116+
117+
button_menu = Pin(BUTTON_MENU, Pin.IN, Pin.PULL_UP)
118+
button_volume = Pin(BUTTON_VOLUME, Pin.IN, Pin.PULL_UP)
119+
button_select = Pin(BUTTON_SELECT, Pin.IN, Pin.PULL_UP)
120+
button_start = Pin(BUTTON_START, Pin.IN, Pin.PULL_UP) # -> ENTER
121+
122+
# PREV <- B | A -> NEXT
123+
button_b = Pin(BUTTON_B, Pin.IN, Pin.PULL_UP)
124+
button_a = Pin(BUTTON_A, Pin.IN, Pin.PULL_UP)
125+
126+
127+
class CrossbarHandler:
128+
# ADC values are around low: ~236 and high ~511
129+
# So the mid value is around (236+511)/2 = 373.5
130+
CROSSBAR_MIN_ADC_LOW = const(100)
131+
CROSSBAR_MIN_ADC_MID = const(370)
132+
133+
def __init__(self, pin, high_key, low_key):
134+
self.adc = ADC(Pin(pin, mode=Pin.IN))
135+
self.adc.width(ADC.WIDTH_9BIT)
136+
self.adc.atten(ADC.ATTN_11DB)
137+
138+
self.high_key = high_key
139+
self.low_key = low_key
140+
141+
def poll(self):
142+
value = self.adc.read()
143+
if value > self.CROSSBAR_MIN_ADC_LOW:
144+
if value > self.CROSSBAR_MIN_ADC_MID:
145+
return self.high_key
146+
elif value < self.CROSSBAR_MIN_ADC_MID:
147+
return self.low_key
148+
149+
150+
class Crossbar:
151+
def __init__(self, *, up, down, left, right):
152+
self.joy_x = CrossbarHandler(CROSSBAR_X, high_key=left, low_key=right)
153+
self.joy_y = CrossbarHandler(CROSSBAR_Y, high_key=up, low_key=down)
154+
155+
def poll(self):
156+
crossbar_pressed = self.joy_x.poll() or self.joy_y.poll()
157+
return crossbar_pressed
158+
159+
160+
# see: internal_filesystem/lib/mpos/indev/mpos_sdl_keyboard.py
161+
# lv.KEY.UP
162+
# lv.KEY.LEFT - lv.KEY.RIGHT
163+
# lv.KEY.DOWN
164+
#
165+
crossbar = Crossbar(
166+
up=lv.KEY.UP, down=lv.KEY.DOWN, left=lv.KEY.LEFT, right=lv.KEY.RIGHT
167+
)
168+
169+
REPEAT_INITIAL_DELAY_MS = 300 # Delay before first repeat
170+
REPEAT_RATE_MS = 100 # Interval between repeats
171+
next_repeat = None # Used for auto-repeat key handling
172+
173+
174+
def input_callback(indev, data):
175+
global next_repeat
176+
177+
current_key = None
178+
179+
if crossbar_pressed := crossbar.poll():
180+
current_key = crossbar_pressed
181+
182+
elif button_menu.value() == 0:
183+
current_key = lv.KEY.ESC
184+
elif button_volume.value() == 0:
185+
print("Volume button pressed -> reset")
186+
machine.reset()
187+
elif button_select.value() == 0:
188+
current_key = lv.KEY.BACKSPACE
189+
elif button_start.value() == 0:
190+
current_key = lv.KEY.ENTER
191+
192+
elif button_b.value() == 0:
193+
current_key = lv.KEY.PREV
194+
elif button_a.value() == 0:
195+
current_key = lv.KEY.NEXT
196+
else:
197+
# No crossbar/buttons pressed
198+
if data.key: # A key was previously pressed and now released
199+
# print(f"Key {data.key=} released")
200+
data.key = 0
201+
data.state = lv.INDEV_STATE.RELEASED
202+
next_repeat = None
203+
blue_led.off()
204+
return
205+
206+
# A key is currently pressed
207+
208+
blue_led.on() # Blink on key press and auto repeat for feedback
209+
210+
current_time = time.ticks_ms()
211+
repeat = current_time > next_repeat if next_repeat else False # Auto repeat?
212+
if repeat or current_key != data.key:
213+
print(f"Key {current_key} pressed {repeat=}")
214+
215+
data.key = current_key
216+
data.state = lv.INDEV_STATE.PRESSED
217+
218+
if current_key == lv.KEY.ESC: # Handle ESC for back navigation
219+
mpos.ui.back_screen()
220+
elif current_key == lv.KEY.RIGHT:
221+
mpos.ui.focus_direction.move_focus_direction(90)
222+
elif current_key == lv.KEY.LEFT:
223+
mpos.ui.focus_direction.move_focus_direction(270)
224+
elif current_key == lv.KEY.UP:
225+
mpos.ui.focus_direction.move_focus_direction(0)
226+
elif current_key == lv.KEY.DOWN:
227+
mpos.ui.focus_direction.move_focus_direction(180)
228+
229+
if not repeat:
230+
# Initial press: Delay before first repeat
231+
next_repeat = current_time + REPEAT_INITIAL_DELAY_MS
232+
else:
233+
# Faster auto repeat after initial press
234+
next_repeat = current_time + REPEAT_RATE_MS
235+
blue_led.off() # Blink the LED, too
236+
237+
238+
group = lv.group_create()
239+
group.set_default()
240+
241+
# Create and set up the input device
242+
indev = lv.indev_create()
243+
indev.set_type(lv.INDEV_TYPE.KEYPAD)
244+
indev.set_read_cb(input_callback)
245+
indev.set_group(
246+
group
247+
) # is this needed? maybe better to move the default group creation to main.py so it's available everywhere...
248+
disp = lv.display_get_default() # NOQA
249+
indev.set_display(disp) # different from display
250+
indev.enable(True) # NOQA
251+
InputManager.register_indev(indev)
252+
253+
print("odroid_go.py finished")

internal_filesystem/lib/mpos/main.py

Lines changed: 65 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -39,38 +39,85 @@ def single_address_i2c_scan(i2c_bus, address):
3939
Returns:
4040
True if a device responds at the specified address, False otherwise
4141
"""
42+
print(f"Attempt to write a single byte to I2C bus address 0x{address:02x}...")
4243
try:
4344
# Attempt to write a single byte to the address
4445
# This will raise an exception if no device responds
45-
i2c_bus.writeto(address, b'')
46+
i2c_bus.writeto(address, b"")
47+
print("Write test successful")
4648
return True
47-
except OSError:
48-
# No device at this address
49+
except OSError as e:
50+
print(f"No device at this address: {e}")
4951
return False
5052
except Exception as e:
5153
# Handle any other exceptions gracefully
52-
print(f"single_address_i2c_scan: error scanning address 0x{address:02x}: {e}")
54+
print(f"scan error: {e}")
5355
return False
5456

57+
58+
def fail_save_i2c(sda, scl):
59+
from machine import I2C, Pin
60+
61+
print(f"Try to I2C initialized on {sda=} {scl=}")
62+
try:
63+
i2c0 = I2C(0, sda=Pin(sda), scl=Pin(scl))
64+
except Exception as e:
65+
print(f"Failed: {e}")
66+
return None
67+
else:
68+
print("OK")
69+
return i2c0
70+
71+
72+
def check_pins(*pins):
73+
from machine import Pin
74+
75+
print(f"Test {pins=}...")
76+
for pin in pins:
77+
try:
78+
Pin(pin)
79+
except Exception as e:
80+
print(f"Failed to initialize {pin=}: {e}")
81+
return True
82+
print("All pins initialized successfully")
83+
return True
84+
85+
5586
def detect_board():
5687
import sys
5788
if sys.platform == "linux" or sys.platform == "darwin": # linux and macOS
5889
return "linux"
5990
elif sys.platform == "esp32":
60-
from machine import Pin, I2C
61-
62-
i2c0 = I2C(0, sda=Pin(39), scl=Pin(38))
63-
if single_address_i2c_scan(i2c0, 0x14) or single_address_i2c_scan(i2c0, 0x5D): # "ghost" or real GT911 touch screen
64-
return "matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660"
65-
66-
i2c0 = I2C(0, sda=Pin(48), scl=Pin(47)) # IO48 is floating on matouch and therefore, using that for I2C will find many devices, so do this after matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660
67-
if single_address_i2c_scan(i2c0, 0x15) and single_address_i2c_scan(i2c0, 0x6B): # CST816S touch screen and IMU
68-
return "waveshare_esp32_s3_touch_lcd_2"
69-
70-
i2c0 = I2C(0, sda=Pin(9), scl=Pin(18))
71-
if single_address_i2c_scan(i2c0, 0x6B): # IMU (plus possibly the Communicator's LANA TNY at 0x38)
72-
return "fri3d_2024"
73-
91+
print("Detecting ESP32 board by scanning I2C addresses...")
92+
93+
print("matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660 ?")
94+
if i2c0 := fail_save_i2c(sda=39, scl=38):
95+
if single_address_i2c_scan(i2c0, 0x14) or single_address_i2c_scan(
96+
i2c0, 0x5D
97+
):
98+
# "ghost" or real GT911 touch screen
99+
return "matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660"
100+
101+
print("waveshare_esp32_s3_touch_lcd_2 ?")
102+
if i2c0 := fail_save_i2c(sda=48, scl=47):
103+
# IO48 is floating on matouch and therefore, using that for I2C will find many devices, so do this after matouch_esp32_s3_spi_ips_2_8_with_camera_ov3660
104+
if single_address_i2c_scan(i2c0, 0x15) and single_address_i2c_scan(
105+
i2c0, 0x6B
106+
):
107+
# CST816S touch screen and IMU
108+
return "waveshare_esp32_s3_touch_lcd_2"
109+
110+
print("odroid_go ?")
111+
if check_pins(0, 13, 27, 39):
112+
return "odroid_go"
113+
114+
print("fri3d_2024 ?")
115+
if i2c0 := fail_save_i2c(sda=9, scl=18):
116+
# IMU (plus possibly the Communicator's LANA TNY at 0x38)
117+
if single_address_i2c_scan(i2c0, 0x6B):
118+
return "fri3d_2024"
119+
120+
print("Fallback to fri3d_2026")
74121
# default: if single_address_i2c_scan(i2c0, 0x6A): # IMU but currently not installed
75122
return "fri3d_2026"
76123

internal_filesystem/main.py

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,28 @@
22

33
# Make sure the storage partition's lib/ is first in the path, so whatever is placed there overrides frozen libraries.
44
# This allows any build to be used for development as well, just by overriding the libraries in lib/
5+
import gc
6+
import os
57
import sys
6-
sys.path.insert(0, 'lib')
78

8-
print(f"Minimal main.py importing mpos.main with sys.path: {sys.path}")
9-
import mpos.main
9+
sys.path.insert(0, "lib")
10+
11+
print(f"{sys.version=}")
12+
print(f"{sys.implementation=}")
13+
14+
15+
print("Check free space on root filesystem:")
16+
stat = os.statvfs("/")
17+
total_space = stat[0] * stat[2]
18+
free_space = stat[0] * stat[3]
19+
used_space = total_space - free_space
20+
print(f"{total_space=} / {used_space=} / {free_space=} bytes")
21+
22+
23+
gc.collect()
24+
print(
25+
f"RAM: {gc.mem_free()} free, {gc.mem_alloc()} allocated, {gc.mem_alloc() + gc.mem_free()} total"
26+
)
27+
28+
print("Passing execution over to mpos.main")
29+
import mpos.main # noqa: F401

ruff.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
[format]
2+
quote-style = "double"

0 commit comments

Comments
 (0)