Compare commits

...

14 Commits

Author SHA1 Message Date
Philippe G
8c33acfd35 tweak dac_controlset timing - release 2020-09-01 16:24:36 -07:00
Sebastien
0222a34286 Merge branch 'master-cmake' of https://github.com/sle118/squeezelite-esp32.git into master-cmake 2020-09-01 16:45:11 -04:00
Sebastien
a90c9802ab Fix commands not working in telnet #43 - release 2020-09-01 16:43:48 -04:00
Philippe G
94da8ca950 i2c timeout change + remove some wifi test code used for led fix - release 2020-09-01 13:40:25 -07:00
Philippe G
9a9a4fef65 Merge branch 'master-cmake' of https://github.com/sle118/squeezelite-esp32 into master-cmake 2020-09-01 12:24:11 -07:00
Philippe G
3a7b1f48c7 change a bit log 2020-09-01 12:24:09 -07:00
Sebastien
a46bbb409f Fixes #50 - Green led flash state reset on wifi connect - release 2020-09-01 15:11:45 -04:00
Sebastien
08d16c2ca2 Led configuration wasn't correctly reported in logs 2020-09-01 12:03:31 -04:00
Philippe G
b501352ddc Merge branch 'master-cmake' of https://github.com/sle118/squeezelite-esp32 into master-cmake 2020-08-31 15:36:21 -07:00
Philippe G
1c51598366 shift i2c address by 1 for consistency - release 2020-08-31 15:36:17 -07:00
Sebastien
db839a9ccd Add new status field: is_i2c_locked to help with the new config page 2020-08-31 16:56:54 -04:00
Philippe G
5aba426b98 no mutex needed for polling - release 2020-08-30 12:30:30 -07:00
Philippe G
6c184efa92 faster & simpler solution for stream poll() - release 2020-08-30 11:22:03 -07:00
Philippe G
028a090864 update plugin files 2020-08-29 22:20:26 -07:00
14 changed files with 101 additions and 70 deletions

View File

@@ -35,7 +35,7 @@ static bool I2CDefaultWriteData( struct GDS_Device* Device, const uint8_t* Data,
bool GDS_I2CInit( int PortNumber, int SDA, int SCL, int Speed ) { bool GDS_I2CInit( int PortNumber, int SDA, int SCL, int Speed ) {
I2CPortNumber = PortNumber; I2CPortNumber = PortNumber;
I2CWait = pdMS_TO_TICKS( Speed ? Speed / 4000 : 100 ); I2CWait = pdMS_TO_TICKS( Speed ? (250 * 250000) / Speed : 250 );
if (SDA != -1 && SCL != -1) { if (SDA != -1 && SCL != -1) {
i2c_config_t Config = { 0 }; i2c_config_t Config = { 0 };

View File

@@ -85,7 +85,7 @@ static void vCallbackFunction( TimerHandle_t xTimer ) {
bool led_blink_core(int idx, int ontime, int offtime, bool pushed) { bool led_blink_core(int idx, int ontime, int offtime, bool pushed) {
if (!leds[idx].gpio || leds[idx].gpio < 0 ) return false; if (!leds[idx].gpio || leds[idx].gpio < 0 ) return false;
ESP_LOGD(TAG,"led_blink_core"); ESP_LOGD(TAG,"led_blink_core %d on:%d off:%d, pushed:%u", idx, ontime, offtime, pushed);
if (leds[idx].timer) { if (leds[idx].timer) {
// normal requests waits if a pop is pending // normal requests waits if a pop is pending
if (!pushed && leds[idx].pushed) { if (!pushed && leds[idx].pushed) {
@@ -231,8 +231,7 @@ void led_svc_init(void) {
#ifndef CONFIG_LED_LOCKED #ifndef CONFIG_LED_LOCKED
parse_set_GPIO(set_led_gpio); parse_set_GPIO(set_led_gpio);
#endif #endif
ESP_LOGI(TAG,"Configuring LEDs green:%d (active:%d %d%%), red:%d (active:%d %d%%)", green.gpio, green.active, green.pwm, green.gpio, green.active, green.pwm );
char *nvs_item = config_alloc_get(NVS_TYPE_STR, "led_brightness"), *p; char *nvs_item = config_alloc_get(NVS_TYPE_STR, "led_brightness"), *p;
if (nvs_item) { if (nvs_item) {
if ((p = strcasestr(nvs_item, "green")) != NULL) green.pwm = atoi(strchr(p, '=') + 1); if ((p = strcasestr(nvs_item, "green")) != NULL) green.pwm = atoi(strchr(p, '=') + 1);
@@ -242,4 +241,6 @@ void led_svc_init(void) {
led_config(LED_GREEN, green.gpio, green.active, green.pwm); led_config(LED_GREEN, green.gpio, green.active, green.pwm);
led_config(LED_RED, red.gpio, red.active, red.pwm); led_config(LED_RED, red.gpio, red.active, red.pwm);
ESP_LOGI(TAG,"Configuring LEDs green:%d (active:%d %d%%), red:%d (active:%d %d%%)", green.gpio, green.active, green.pwm, red.gpio, red.active, red.pwm );
} }

View File

@@ -128,12 +128,12 @@ static esp_err_t i2c_write_reg(uint8_t reg, uint8_t val) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd); i2c_master_start(cmd);
i2c_master_write_byte(cmd, i2c_addr | I2C_MASTER_WRITE, I2C_MASTER_NACK); i2c_master_write_byte(cmd, (i2c_addr << 1) | I2C_MASTER_WRITE, I2C_MASTER_NACK);
i2c_master_write_byte(cmd, reg, I2C_MASTER_NACK); i2c_master_write_byte(cmd, reg, I2C_MASTER_NACK);
i2c_master_write_byte(cmd, val, I2C_MASTER_NACK); i2c_master_write_byte(cmd, val, I2C_MASTER_NACK);
i2c_master_stop(cmd); i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(i2c_port, cmd, 1000 / portTICK_RATE_MS); ret = i2c_master_cmd_begin(i2c_port, cmd, 100 / portTICK_RATE_MS);
i2c_cmd_link_delete(cmd); i2c_cmd_link_delete(cmd);
if (ret != ESP_OK) { if (ret != ESP_OK) {
@@ -153,15 +153,15 @@ static uint8_t i2c_read_reg(uint8_t reg) {
i2c_cmd_handle_t cmd = i2c_cmd_link_create(); i2c_cmd_handle_t cmd = i2c_cmd_link_create();
i2c_master_start(cmd); i2c_master_start(cmd);
i2c_master_write_byte(cmd, i2c_addr | I2C_MASTER_WRITE, I2C_MASTER_NACK); i2c_master_write_byte(cmd, (i2c_addr << 1) | I2C_MASTER_WRITE, I2C_MASTER_NACK);
i2c_master_write_byte(cmd, reg, I2C_MASTER_NACK); i2c_master_write_byte(cmd, reg, I2C_MASTER_NACK);
i2c_master_start(cmd); i2c_master_start(cmd);
i2c_master_write_byte(cmd, i2c_addr | I2C_MASTER_READ, I2C_MASTER_NACK); i2c_master_write_byte(cmd, (i2c_addr << 1) | I2C_MASTER_READ, I2C_MASTER_NACK);
i2c_master_read_byte(cmd, &data, I2C_MASTER_NACK); i2c_master_read_byte(cmd, &data, I2C_MASTER_NACK);
i2c_master_stop(cmd); i2c_master_stop(cmd);
ret = i2c_master_cmd_begin(i2c_port, cmd, 1000 / portTICK_RATE_MS); ret = i2c_master_cmd_begin(i2c_port, cmd, 100 / portTICK_RATE_MS);
i2c_cmd_link_delete(cmd); i2c_cmd_link_delete(cmd);
if (ret != ESP_OK) { if (ret != ESP_OK) {

View File

@@ -46,27 +46,17 @@ struct buffer *streambuf = &buf;
#define UNLOCK mutex_unlock(streambuf->mutex) #define UNLOCK mutex_unlock(streambuf->mutex)
/* /*
After a lot of hesitation, I've added that "poll mutex" to prevent When LMS sends a close/open sequence very quickly, the stream thread might
socket from being allocated while we are still in poll(). The issue still be waiting in the poll() on the closed socket. It is never recommended
happens is we have a close quickly followed by an open, we might still to have a thread closing a socket used by another thread but it works, as
be in the poll() and simple OS fail as they re-allocate the same socket opposed to an infinite select().
on which a thread is still waiting. In stream_sock() a new socket is created and full OS will allocate a different
Ideally, you want to set the lock in the disconnect() but that would mean one but on RTOS and simple IP stack, the same might be re-used and that causes
very often we'd have to always wait for the end of the poll(), i.e. up to an exception as a thread is already waiting on a newly allocated socket
100ms for nothing most of the time where if it is in the open(), it is A simple variable that forces stream_sock() to wait until we are out of poll()
less elegant as closing a socket on which there is a poll() is not good is enough and much faster than a mutex
but it's more efficient as it is very rare that you'd have an open() less
then 100ms after a close()
*/ */
#if EMBEDDED static bool polling;
static mutex_type poll_mutex;
#define LOCK_L mutex_lock(poll_mutex)
#define UNLOCK_L mutex_unlock(poll_mutex)
#else
#define LOCK_L
#define UNLOCK_L
#endif
static sockfd fd; static sockfd fd;
struct streamstate stream; struct streamstate stream;
@@ -209,7 +199,6 @@ static void *stream_thread() {
} else { } else {
LOCK_L;
pollinfo.fd = fd; pollinfo.fd = fd;
pollinfo.events = POLLIN; pollinfo.events = POLLIN;
if (stream.state == SEND_HEADERS) { if (stream.state == SEND_HEADERS) {
@@ -218,10 +207,12 @@ static void *stream_thread() {
} }
UNLOCK; UNLOCK;
// no mutex needed - we just want to know if we are inside poll()
polling = true;
if (_poll(ssl, &pollinfo, 100)) { if (_poll(ssl, &pollinfo, 100)) {
UNLOCK_L; polling = false;
LOCK; LOCK;
// check socket has not been closed while in poll // check socket has not been closed while in poll
@@ -374,7 +365,7 @@ static void *stream_thread() {
UNLOCK; UNLOCK;
} else { } else {
UNLOCK_L; polling = false;
LOG_SDEBUG("poll timeout"); LOG_SDEBUG("poll timeout");
} }
} }
@@ -427,9 +418,6 @@ void stream_init(log_level level, unsigned stream_buf_size) {
*stream.header = '\0'; *stream.header = '\0';
fd = -1; fd = -1;
#if EMBEDDED
mutex_create_p(poll_mutex);
#endif
#if LINUX || FREEBSD #if LINUX || FREEBSD
touch_memory(streambuf->buf, streambuf->size); touch_memory(streambuf->buf, streambuf->size);
@@ -459,9 +447,6 @@ void stream_close(void) {
#endif #endif
free(stream.header); free(stream.header);
buf_destroy(streambuf); buf_destroy(streambuf);
#if EMBEDDED
mutex_destroy(poll_mutex);
#endif
} }
void stream_file(const char *header, size_t header_len, unsigned threshold) { void stream_file(const char *header, size_t header_len, unsigned threshold) {
@@ -503,9 +488,12 @@ void stream_file(const char *header, size_t header_len, unsigned threshold) {
void stream_sock(u32_t ip, u16_t port, const char *header, size_t header_len, unsigned threshold, bool cont_wait) { void stream_sock(u32_t ip, u16_t port, const char *header, size_t header_len, unsigned threshold, bool cont_wait) {
struct sockaddr_in addr; struct sockaddr_in addr;
LOCK_L; #if EMBEDDED
// wait till we are not polling anymore
while (polling && running) { usleep(10000); }
#endif
int sock = socket(AF_INET, SOCK_STREAM, 0); int sock = socket(AF_INET, SOCK_STREAM, 0);
UNLOCK_L;
if (sock < 0) { if (sock < 0) {
LOG_ERROR("failed to create socket"); LOG_ERROR("failed to create socket");

View File

@@ -253,6 +253,10 @@ void process_received_data(const char * buffer, size_t size){
if(bMirrorToUART){ if(bMirrorToUART){
write(uart_fd, command, size); write(uart_fd, command, size);
} }
for(int i=strlen(command);i>=0;i--){
// strip any cr/lf
if(command[i]== '\n' || command[i]== '\r') command[i]= '\0';
}
run_command((char *)command); run_command((char *)command);
} }
free(command); free(command);

View File

@@ -462,7 +462,11 @@ cJSON * wifi_manager_get_basic_info(cJSON **old){
cJSON_AddNumberToObject(root,"Voltage", battery_value_svc()); cJSON_AddNumberToObject(root,"Voltage", battery_value_svc());
cJSON_AddNumberToObject(root,"disconnect_count", num_disconnect ); cJSON_AddNumberToObject(root,"disconnect_count", num_disconnect );
cJSON_AddNumberToObject(root,"avg_conn_time", num_disconnect>0?(total_connected_time/num_disconnect):0 ); cJSON_AddNumberToObject(root,"avg_conn_time", num_disconnect>0?(total_connected_time/num_disconnect):0 );
#if CONFIG_I2C_LOCKED
cJSON_AddTrueToObject(root, "is_i2c_locked");
#else
cJSON_AddFalseToObject(root, "is_i2c_locked");
#endif
ESP_LOGV(TAG, "wifi_manager_get_basic_info done"); ESP_LOGV(TAG, "wifi_manager_get_basic_info done");
return root; return root;
} }

View File

@@ -44,7 +44,7 @@ menu "Squeezelite-ESP32"
string string
default "model=TAS57xx,bck=33,ws=25,do=32,sda=27,scl=26,mute=14:0" if SQUEEZEAMP default "model=TAS57xx,bck=33,ws=25,do=32,sda=27,scl=26,mute=14:0" if SQUEEZEAMP
default "model=AC101,bck=27,ws=26,do=25,di=35,sda=33,scl=32" if A1S default "model=AC101,bck=27,ws=26,do=25,di=35,sda=33,scl=32" if A1S
default "model=I2S,bck=26,ws=25,do=33,i2c=106,sda=21,scl=22" if TWATCH2020 default "model=I2S,bck=26,ws=25,do=33,i2c=53,sda=21,scl=22" if TWATCH2020
default "" default ""
config SPDIF_CONFIG config SPDIF_CONFIG
string string

View File

@@ -447,7 +447,7 @@ void app_main()
/* start the wifi manager */ /* start the wifi manager */
ESP_LOGD(TAG,"Blinking led"); ESP_LOGD(TAG,"Blinking led");
led_blink(LED_GREEN, 250, 250); led_blink_pushed(LED_GREEN, 250, 250);
if(bypass_wifi_manager){ if(bypass_wifi_manager){
ESP_LOGW(TAG,"*******************************************************************************************"); ESP_LOGW(TAG,"*******************************************************************************************");

Binary file not shown.

View File

@@ -78,7 +78,7 @@ sub displayWidth {
if ($display->widthOverride) { if ($display->widthOverride) {
my $artwork = $prefs->client($client)->get('artwork'); my $artwork = $prefs->client($client)->get('artwork');
if ($artwork->{'enable'} && $artwork->{'y'} < 32 && ($client->isPlaying || $client->isPaused)) { if ($artwork->{'enable'} && $artwork->{'y'} < 32 && ($client->isPlaying || $client->isPaused)) {
return $artwork->{x} + ($display->modes->[$mode || 0]{_width} || 0); return ($artwork->{x} || $display->widthOverride) + ($display->modes->[$mode || 0]{_width} || 0);
} else { } else {
return $display->widthOverride + ($display->modes->[$mode || 0]{_width} || 0); return $display->widthOverride + ($display->modes->[$mode || 0]{_width} || 0);
} }
@@ -113,9 +113,9 @@ sub build_modes {
my $artwork = $cprefs->get('artwork'); my $artwork = $cprefs->get('artwork');
my $disp_width = $cprefs->get('width') || 128; my $disp_width = $cprefs->get('width') || 128;
# if artwork is in main display, reduce width # if artwork is in main display, reduce width but when artwork is (0,0) fake it
my $width = ($artwork->{'enable'} && $artwork->{'y'} < 32) ? $artwork->{'x'} : $disp_width; my $width = ($artwork->{'enable'} && $artwork->{'y'} < 32 && $artwork->{'x'}) ? $artwork->{'x'} : $disp_width;
my $width_low = ($artwork->{'enable'} && ($artwork->{'y'} >= 32 || $disp_width - $artwork->{'x'} > 32)) ? $artwork->{'x'} : $disp_width; my $width_low = ($artwork->{'enable'} && $artwork->{'x'} && ($artwork->{'y'} >= 32 || $disp_width - $artwork->{'x'} > 32)) ? $artwork->{'x'} : $disp_width;
my $small_VU = $cprefs->get('small_VU'); my $small_VU = $cprefs->get('small_VU');
my $spectrum = $cprefs->get('spectrum'); my $spectrum = $cprefs->get('spectrum');

View File

@@ -13,6 +13,19 @@ my $sprefs = preferences('server');
my $prefs = preferences('plugin.squeezeesp32'); my $prefs = preferences('plugin.squeezeesp32');
my $log = logger('plugin.squeezeesp32'); my $log = logger('plugin.squeezeesp32');
{
__PACKAGE__->mk_accessor('rw', 'tone_update');
}
sub new {
my $class = shift;
my $client = $class->SUPER::new(@_);
$client->init_accessor(
tone_update => 0,
);
return $client;
}
our $defaultPrefs = { our $defaultPrefs = {
'analogOutMode' => 0, 'analogOutMode' => 0,
'bass' => 0, 'bass' => 0,
@@ -44,7 +57,6 @@ sub hasIR { 1 }
# TODO: add in settings when ready # TODO: add in settings when ready
sub hasLineIn { 0 } sub hasLineIn { 0 }
sub hasHeadSubOut { 1 } sub hasHeadSubOut { 1 }
# TODO: LMS sliders are hard-coded in html file from -23 to +23
sub maxTreble { 20 } sub maxTreble { 20 }
sub minTreble { -13 } sub minTreble { -13 }
sub maxBass { 20 } sub maxBass { 20 }
@@ -54,7 +66,7 @@ sub init {
my $client = shift; my $client = shift;
if (!$handlersAdded) { if (!$handlersAdded) {
# Add a handler for line-in/out status changes # Add a handler for line-in/out status changes
Slim::Networking::Slimproto::addHandler( LIOS => \&lineInOutStatus ); Slim::Networking::Slimproto::addHandler( LIOS => \&lineInOutStatus );
@@ -117,25 +129,41 @@ sub playerSettingsFrame {
} }
sub bass { sub bass {
return tone(2, @_); my ($client, $new) = @_;
} my $value = $client->SUPER::bass($new);
sub treble { $client->update_equalizer($value, [2, 1, 3]) if defined $new;
return tone(8, @_);
return $value;
} }
sub tone { sub treble {
my ($center, $client, $value) = @_; my ($client, $new) = @_;
my $equalizer = $prefs->client($client)->get('equalizer'); my $value = $client->SUPER::treble($new);
if (defined($value)) { $client->update_equalizer($value, [8, 9, 7]) if defined $new;
$equalizer->[$center-1] = int($value * 0.2 + 0.5);
$equalizer->[$center] = int($value * 0.7 + 0.5);
$equalizer->[$center+1] = int($value * 0.1 + 0.5);
$prefs->client($client)->set('equalizer', $equalizer);
}
return int($equalizer->[$center-1] * 0.2 + $equalizer->[$center] * 0.7 + $equalizer->[$center+1] * 0.1); return $value;
}
sub update_equalizer {
my ($client, $value, $index) = @_;
return if $client->tone_update;
my $equalizer = $prefs->client($client)->get('equalizer');
$equalizer->[$index->[0]] = $value;
$equalizer->[$index->[1]] = int($value / 2 + 0.5);
$equalizer->[$index->[2]] = int($value / 4 + 0.5);
$prefs->client($client)->set('equalizer', $equalizer);
}
sub update_tones {
my ($client, $equalizer) = @_;
$client->tone_update(1);
$sprefs->client($client)->set('bass', int(($equalizer->[1] * 2 + $equalizer->[2] + $equalizer->[3] * 4) / 7 + 0.5));
$sprefs->client($client)->set('treble', int(($equalizer->[7] * 4 + $equalizer->[8] + $equalizer->[9] * 2) / 7 + 0.5));
$client->tone_update(0);
} }
sub update_artwork { sub update_artwork {
@@ -193,6 +221,11 @@ sub clear_artwork {
if ($artwork && $artwork->{'enable'}) { if ($artwork && $artwork->{'enable'}) {
main::INFOLOG && $log->is_info && $log->info("artwork stop/clear " . $request->getRequestString()); main::INFOLOG && $log->is_info && $log->info("artwork stop/clear " . $request->getRequestString());
$client->pluginData('artwork_md5', ''); $client->pluginData('artwork_md5', '');
# refresh screen and disable artwork when artwork was full screen (hack)
if (!$artwork->{'x'} && !$artwork->{'y'}) {
$client->sendFrame(grfa => \("\x00"x4)) unless $artwork->{'x'} || $artwork->{'y'};
$client->display->update;
}
} }
} }

View File

@@ -31,7 +31,7 @@ sub page {
sub prefs { sub prefs {
my ($class, $client) = @_; my ($class, $client) = @_;
my @prefs; my @prefs;
push @prefs, qw(width small_VU) if $client->displayWidth; push @prefs, qw(width small_VU) if defined $client->displayWidth;
return ($prefs->client($client), @prefs); return ($prefs->client($client), @prefs);
} }
@@ -41,7 +41,7 @@ sub handler {
my ($cprefs, @prefs) = $class->prefs($client); my ($cprefs, @prefs) = $class->prefs($client);
if ($paramRef->{'saveSettings'}) { if ($paramRef->{'saveSettings'}) {
if ($client->displayWidth) { if (defined $client->displayWidth) {
$cprefs->set('small_VU', $paramRef->{'pref_small_VU'} || 15); $cprefs->set('small_VU', $paramRef->{'pref_small_VU'} || 15);
my $spectrum = { my $spectrum = {
scale => $paramRef->{'pref_spectrum_scale'} || 25, scale => $paramRef->{'pref_spectrum_scale'} || 25,
@@ -73,9 +73,10 @@ sub handler {
$equalizer->[$i] = $paramRef->{"pref_equalizer.$i"} || 0; $equalizer->[$i] = $paramRef->{"pref_equalizer.$i"} || 0;
} }
$cprefs->set('equalizer', $equalizer); $cprefs->set('equalizer', $equalizer);
$client->update_tones($equalizer);
} }
if ($client->displayWidth) { if (defined $client->displayWidth) {
# the Settings super class can't handle anything but scalar values # the Settings super class can't handle anything but scalar values
# we need to populate the $paramRef for the other prefs manually # we need to populate the $paramRef for the other prefs manually
$paramRef->{'pref_spectrum'} = $cprefs->get('spectrum'); $paramRef->{'pref_spectrum'} = $cprefs->get('spectrum');

View File

@@ -10,6 +10,6 @@
<name>PLUGIN_SQUEEZEESP32</name> <name>PLUGIN_SQUEEZEESP32</name>
<description>PLUGIN_SQUEEZEESP32_DESC</description> <description>PLUGIN_SQUEEZEESP32_DESC</description>
<module>Plugins::SqueezeESP32::Plugin</module> <module>Plugins::SqueezeESP32::Plugin</module>
<version>0.100</version> <version>0.104</version>
<creator>Philippe</creator> <creator>Philippe</creator>
</extensions> </extensions>

View File

@@ -1,10 +1,10 @@
<?xml version='1.0' standalone='yes'?> <?xml version='1.0' standalone='yes'?>
<extensions> <extensions>
<plugins> <plugins>
<plugin version="0.100" name="SqueezeESP32" minTarget="7.9" maxTarget="*"> <plugin version="0.104" name="SqueezeESP32" minTarget="7.9" maxTarget="*">
<link>https://github.com/sle118/squeezelite-esp32</link> <link>https://github.com/sle118/squeezelite-esp32</link>
<creator>Philippe</creator> <creator>Philippe</creator>
<sha>572fa8afeaa3bc3cfb245b8d42ba05739aec584b</sha> <sha>79e505a30d7b6dbf43893acab176d57438e2a4a1</sha>
<email>philippe_44@outlook.com</email> <email>philippe_44@outlook.com</email>
<desc lang="EN">SqueezeESP32 additional player id (100)</desc> <desc lang="EN">SqueezeESP32 additional player id (100)</desc>
<url>http://github.com/sle118/squeezelite-esp32/raw/master/plugin/SqueezeESP32.zip</url> <url>http://github.com/sle118/squeezelite-esp32/raw/master/plugin/SqueezeESP32.zip</url>